play walkout songs
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
import { createServerFileRoute } from '@tanstack/react-start/server'
|
import { createServerFileRoute } from '@tanstack/react-start/server'
|
||||||
import { SpotifyWebApiClient } from '@/lib/spotify/client'
|
import { SpotifyWebApiClient } from '@/lib/spotify/client'
|
||||||
|
|
||||||
// Helper function to get access token from cookies
|
|
||||||
function getAccessTokenFromCookies(request: Request): string | null {
|
function getAccessTokenFromCookies(request: Request): string | null {
|
||||||
const cookieHeader = request.headers.get('cookie')
|
const cookieHeader = request.headers.get('cookie')
|
||||||
if (!cookieHeader) return null
|
if (!cookieHeader) return null
|
||||||
@@ -28,7 +27,7 @@ export const ServerRoute = createServerFileRoute('/api/spotify/playback').method
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const { action, deviceId, volumePercent } = body
|
const { action, deviceId, volumePercent, trackId, positionMs } = body
|
||||||
|
|
||||||
const spotifyClient = new SpotifyWebApiClient(accessToken)
|
const spotifyClient = new SpotifyWebApiClient(accessToken)
|
||||||
|
|
||||||
@@ -36,6 +35,18 @@ export const ServerRoute = createServerFileRoute('/api/spotify/playback').method
|
|||||||
case 'play':
|
case 'play':
|
||||||
await spotifyClient.play(deviceId)
|
await spotifyClient.play(deviceId)
|
||||||
break
|
break
|
||||||
|
case 'playTrack':
|
||||||
|
if (!trackId) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: 'trackId is required for playTrack action' }),
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
await spotifyClient.playTrack(trackId, deviceId, positionMs)
|
||||||
|
break
|
||||||
case 'pause':
|
case 'pause':
|
||||||
await spotifyClient.pause()
|
await spotifyClient.pause()
|
||||||
break
|
break
|
||||||
@@ -89,7 +100,6 @@ export const ServerRoute = createServerFileRoute('/api/spotify/playback').method
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Playback control error:', error)
|
console.error('Playback control error:', error)
|
||||||
|
|
||||||
// Handle specific Spotify API errors
|
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
if (error.message.includes('NO_ACTIVE_DEVICE')) {
|
if (error.message.includes('NO_ACTIVE_DEVICE')) {
|
||||||
return new Response(
|
return new Response(
|
||||||
@@ -111,7 +121,6 @@ export const ServerRoute = createServerFileRoute('/api/spotify/playback').method
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log the full error details for debugging
|
|
||||||
console.error('Full error details:', {
|
console.error('Full error details:', {
|
||||||
message: error.message,
|
message: error.message,
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
@@ -129,7 +138,6 @@ export const ServerRoute = createServerFileRoute('/api/spotify/playback').method
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// GET endpoint for retrieving current playback state and devices
|
|
||||||
GET: async ({ request }: { request: Request }) => {
|
GET: async ({ request }: { request: Request }) => {
|
||||||
try {
|
try {
|
||||||
const accessToken = getAccessTokenFromCookies(request)
|
const accessToken = getAccessTokenFromCookies(request)
|
||||||
@@ -144,7 +152,7 @@ export const ServerRoute = createServerFileRoute('/api/spotify/playback').method
|
|||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL(request.url)
|
const url = new URL(request.url)
|
||||||
const type = url.searchParams.get('type') // 'state' or 'devices'
|
const type = url.searchParams.get('type')
|
||||||
|
|
||||||
const spotifyClient = new SpotifyWebApiClient(accessToken)
|
const spotifyClient = new SpotifyWebApiClient(accessToken)
|
||||||
|
|
||||||
@@ -167,7 +175,6 @@ export const ServerRoute = createServerFileRoute('/api/spotify/playback').method
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Return both by default
|
|
||||||
const [devices, playbackState] = await Promise.all([
|
const [devices, playbackState] = await Promise.all([
|
||||||
spotifyClient.getDevices(),
|
spotifyClient.getDevices(),
|
||||||
spotifyClient.getPlaybackState(),
|
spotifyClient.getPlaybackState(),
|
||||||
|
|||||||
@@ -165,16 +165,16 @@ export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
|||||||
|
|
||||||
const play = useCallback(async (deviceId?: string) => {
|
const play = useCallback(async (deviceId?: string) => {
|
||||||
if (!authState.isAuthenticated) return;
|
if (!authState.isAuthenticated) return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await makeSpotifyRequest('playback', {
|
await makeSpotifyRequest('playback', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ action: 'play', deviceId }),
|
body: JSON.stringify({ action: 'play', deviceId }),
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(refreshPlaybackState, 500);
|
setTimeout(refreshPlaybackState, 500);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && !error.message.includes('JSON')) {
|
if (error instanceof Error && !error.message.includes('JSON')) {
|
||||||
@@ -186,6 +186,29 @@ export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
|||||||
}
|
}
|
||||||
}, [authState.isAuthenticated]);
|
}, [authState.isAuthenticated]);
|
||||||
|
|
||||||
|
const playTrack = useCallback(async (trackId: string, deviceId?: string, positionMs?: number) => {
|
||||||
|
if (!authState.isAuthenticated) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await makeSpotifyRequest('playback', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ action: 'playTrack', trackId, deviceId, positionMs }),
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(refreshPlaybackState, 500);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && !error.message.includes('JSON')) {
|
||||||
|
setError(error.message);
|
||||||
|
}
|
||||||
|
console.warn('Track playback action completed with warning:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [authState.isAuthenticated]);
|
||||||
|
|
||||||
const pause = useCallback(async () => {
|
const pause = useCallback(async () => {
|
||||||
if (!authState.isAuthenticated) return;
|
if (!authState.isAuthenticated) return;
|
||||||
|
|
||||||
@@ -422,6 +445,7 @@ export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
|||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
play,
|
play,
|
||||||
|
playTrack,
|
||||||
pause,
|
pause,
|
||||||
skipNext,
|
skipNext,
|
||||||
skipPrevious,
|
skipPrevious,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { ActionIcon, Card, Flex, Text, Stack, Indicator } from "@mantine/core";
|
import { ActionIcon, Card, Flex, Text, Indicator } from "@mantine/core";
|
||||||
import { PlayIcon, PencilIcon, SpeakerHighIcon } from "@phosphor-icons/react";
|
import { PlayIcon, PencilIcon, SpeakerHighIcon } from "@phosphor-icons/react";
|
||||||
import React, { useCallback, useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { MatchSlot } from "./match-slot";
|
import { MatchSlot } from "./match-slot";
|
||||||
import { Match } from "@/features/matches/types";
|
import { Match } from "@/features/matches/types";
|
||||||
|
import { Team } from "@/features/teams/types";
|
||||||
import { useSheet } from "@/hooks/use-sheet";
|
import { useSheet } from "@/hooks/use-sheet";
|
||||||
import { MatchForm } from "./match-form";
|
import { MatchForm } from "./match-form";
|
||||||
import Sheet from "@/components/sheet/sheet";
|
import Sheet from "@/components/sheet/sheet";
|
||||||
@@ -10,6 +11,7 @@ import { useServerMutation } from "@/lib/tanstack-query/hooks";
|
|||||||
import { endMatch, startMatch } from "@/features/matches/server";
|
import { endMatch, startMatch } from "@/features/matches/server";
|
||||||
import { tournamentKeys } from "@/features/tournaments/queries";
|
import { tournamentKeys } from "@/features/tournaments/queries";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useSpotifyPlayback } from "@/lib/spotify/hooks";
|
||||||
|
|
||||||
interface MatchCardProps {
|
interface MatchCardProps {
|
||||||
match: Match;
|
match: Match;
|
||||||
@@ -24,6 +26,7 @@ export const MatchCard: React.FC<MatchCardProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const editSheet = useSheet();
|
const editSheet = useSheet();
|
||||||
|
const { playTrack, pause } = useSpotifyPlayback();
|
||||||
const homeSlot = useMemo(
|
const homeSlot = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
from: orders[match.home_from_lid],
|
from: orders[match.home_from_lid],
|
||||||
@@ -65,6 +68,8 @@ export const MatchCard: React.FC<MatchCardProps> = ({
|
|||||||
[showControls, match.status]
|
[showControls, match.status]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const hasWalkoutData = showControls && match.home && match.away && 'song_id' in match.home && 'song_id' in match.away;
|
||||||
|
|
||||||
const start = useServerMutation({
|
const start = useServerMutation({
|
||||||
mutationFn: startMatch,
|
mutationFn: startMatch,
|
||||||
successMessage: "Match started!",
|
successMessage: "Match started!",
|
||||||
@@ -84,19 +89,13 @@ export const MatchCard: React.FC<MatchCardProps> = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleStart = useCallback(async () => {
|
|
||||||
await start.mutate({
|
|
||||||
data: match.id,
|
|
||||||
});
|
|
||||||
}, [match]);
|
|
||||||
|
|
||||||
const handleFormSubmit = useCallback(
|
const handleFormSubmit = useCallback(
|
||||||
async (data: {
|
async (data: {
|
||||||
home_cups: number;
|
home_cups: number;
|
||||||
away_cups: number;
|
away_cups: number;
|
||||||
ot_count: number;
|
ot_count: number;
|
||||||
}) => {
|
}) => {
|
||||||
await end.mutate({
|
end.mutate({
|
||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
matchId: match.id,
|
matchId: match.id,
|
||||||
@@ -107,12 +106,14 @@ export const MatchCard: React.FC<MatchCardProps> = ({
|
|||||||
[match.id, editSheet]
|
[match.id, editSheet]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSpeakerClick = useCallback(() => {
|
const speak = useCallback((text: string): Promise<void> => {
|
||||||
if ("speechSynthesis" in window && match.home?.name && match.away?.name) {
|
return new Promise((resolve) => {
|
||||||
const utterance = new SpeechSynthesisUtterance(
|
if (!("speechSynthesis" in window)) {
|
||||||
`${match.home.name} vs. ${match.away.name}`
|
resolve();
|
||||||
);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const utterance = new SpeechSynthesisUtterance(text);
|
||||||
const voices = window.speechSynthesis.getVoices();
|
const voices = window.speechSynthesis.getVoices();
|
||||||
|
|
||||||
const preferredVoice =
|
const preferredVoice =
|
||||||
@@ -130,9 +131,71 @@ export const MatchCard: React.FC<MatchCardProps> = ({
|
|||||||
utterance.volume = 0.8;
|
utterance.volume = 0.8;
|
||||||
utterance.pitch = 1.0;
|
utterance.pitch = 1.0;
|
||||||
|
|
||||||
|
utterance.onend = () => resolve();
|
||||||
|
utterance.onerror = () => resolve();
|
||||||
|
|
||||||
window.speechSynthesis.speak(utterance);
|
window.speechSynthesis.speak(utterance);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const playTeamWalkout = useCallback((team: Team): Promise<void> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const songDuration = (team.song_end - team.song_start) * 1000;
|
||||||
|
|
||||||
|
playTrack(team.song_id, undefined, team.song_start * 1000);
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
await pause();
|
||||||
|
resolve();
|
||||||
|
}, songDuration);
|
||||||
|
});
|
||||||
|
}, [playTrack, pause]);
|
||||||
|
|
||||||
|
const handleSpeakerClick = useCallback(async () => {
|
||||||
|
if (!hasWalkoutData || !match.home?.name || !match.away?.name) {
|
||||||
|
await speak(`${match.home?.name || "Home"} vs. ${match.away?.name || "Away"}`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}, [match.home?.name, match.away?.name]);
|
|
||||||
|
try {
|
||||||
|
const homeTeam = match.home as Team;
|
||||||
|
const awayTeam = match.away as Team;
|
||||||
|
|
||||||
|
await playTeamWalkout(homeTeam);
|
||||||
|
await speak(homeTeam.name);
|
||||||
|
await speak("versus");
|
||||||
|
await playTeamWalkout(awayTeam);
|
||||||
|
await speak(awayTeam.name);
|
||||||
|
await speak("have fun, good luck!");
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Walkout sequence error:', error);
|
||||||
|
await speak(`${match.home.name} vs. ${match.away.name}`);
|
||||||
|
}
|
||||||
|
}, [hasWalkoutData, match.home, match.away, speak, playTeamWalkout]);
|
||||||
|
|
||||||
|
const handleStart = useCallback(async () => {
|
||||||
|
start.mutate({
|
||||||
|
data: match.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Play walkout sequence after starting the match
|
||||||
|
if (hasWalkoutData && match.home?.name && match.away?.name) {
|
||||||
|
try {
|
||||||
|
const homeTeam = match.home as Team;
|
||||||
|
const awayTeam = match.away as Team;
|
||||||
|
|
||||||
|
await playTeamWalkout(homeTeam);
|
||||||
|
await speak(homeTeam.name);
|
||||||
|
await speak("versus");
|
||||||
|
await playTeamWalkout(awayTeam);
|
||||||
|
await speak(awayTeam.name);
|
||||||
|
await speak("have fun, good luck!");
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Auto-walkout sequence error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [match, start, hasWalkoutData, playTeamWalkout, speak]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="row" align="center" justify="end" gap={8}>
|
<Flex direction="row" align="center" justify="end" gap={8}>
|
||||||
@@ -175,7 +238,7 @@ export const MatchCard: React.FC<MatchCardProps> = ({
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showControls && (
|
{showControls && match.status !== "tbd" && (
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
pos="absolute"
|
pos="absolute"
|
||||||
bottom={-2}
|
bottom={-2}
|
||||||
@@ -210,6 +273,7 @@ export const MatchCard: React.FC<MatchCardProps> = ({
|
|||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{showEditButton && (
|
{showEditButton && (
|
||||||
<Flex direction="column" justify="center" align="center">
|
<Flex direction="column" justify="center" align="center">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { TeamInfo } from "../teams/types";
|
import { TeamInfo, Team } from "../teams/types";
|
||||||
import { TournamentInfo } from "../tournaments/types";
|
import { TournamentInfo } from "../tournaments/types";
|
||||||
|
|
||||||
export type MatchStatus = "tbd" | "ready" | "started" | "ended";
|
export type MatchStatus = "tbd" | "ready" | "started" | "ended";
|
||||||
@@ -23,8 +23,8 @@ export interface Match {
|
|||||||
is_losers_bracket: boolean;
|
is_losers_bracket: boolean;
|
||||||
status: MatchStatus;
|
status: MatchStatus;
|
||||||
tournament: TournamentInfo;
|
tournament: TournamentInfo;
|
||||||
home?: TeamInfo;
|
home?: TeamInfo | Team;
|
||||||
away?: TeamInfo;
|
away?: TeamInfo | Team;
|
||||||
created: string;
|
created: string;
|
||||||
updated: string;
|
updated: string;
|
||||||
home_seed?: number;
|
home_seed?: number;
|
||||||
|
|||||||
@@ -32,9 +32,10 @@ export const updateTournament = createServerFn()
|
|||||||
export const getTournament = createServerFn()
|
export const getTournament = createServerFn()
|
||||||
.validator(z.string())
|
.validator(z.string())
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
.handler(async ({ data: tournamentId }) =>
|
.handler(async ({ data: tournamentId, context }) => {
|
||||||
toServerResult(() => pbAdmin.getTournament(tournamentId))
|
const isAdmin = context.roles.includes("Admin");
|
||||||
);
|
return toServerResult(() => pbAdmin.getTournament(tournamentId, isAdmin));
|
||||||
|
});
|
||||||
|
|
||||||
export const getCurrentTournament = createServerFn()
|
export const getCurrentTournament = createServerFn()
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export function createPlayersService(pb: PocketBase) {
|
|||||||
expand: "tournament,home,away",
|
expand: "tournament,home,away",
|
||||||
});
|
});
|
||||||
|
|
||||||
return result.map(transformMatch);
|
return result.map((match) => transformMatch(match));
|
||||||
},
|
},
|
||||||
|
|
||||||
async getUnenrolledPlayers(tournamentId: string): Promise<Player[]> {
|
async getUnenrolledPlayers(tournamentId: string): Promise<Player[]> {
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export function createTeamsService(pb: PocketBase) {
|
|||||||
expand: "tournament,home,away",
|
expand: "tournament,home,away",
|
||||||
});
|
});
|
||||||
|
|
||||||
return result.map(transformMatch);
|
return result.map((match) => transformMatch(match));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ import { PlayerInfo } from "@/features/players/types";
|
|||||||
|
|
||||||
export function createTournamentsService(pb: PocketBase) {
|
export function createTournamentsService(pb: PocketBase) {
|
||||||
return {
|
return {
|
||||||
async getTournament(id: string): Promise<Tournament> {
|
async getTournament(id: string, isAdmin: boolean = false): Promise<Tournament> {
|
||||||
const result = await pb.collection("tournaments").getOne(id, {
|
const result = await pb.collection("tournaments").getOne(id, {
|
||||||
expand: "teams, teams.players, matches, matches.tournament, matches.home, matches.away, matches.home.players, matches.away.players",
|
expand: "teams, teams.players, matches, matches.tournament, matches.home, matches.away, matches.home.players, matches.away.players",
|
||||||
});
|
});
|
||||||
return transformTournament(result);
|
return transformTournament(result, isAdmin);
|
||||||
},
|
},
|
||||||
async getMostRecentTournament(): Promise<Tournament> {
|
async getMostRecentTournament(): Promise<Tournament> {
|
||||||
const result = await pb
|
const result = await pb
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function transformTeamInfo(record: any): TeamInfo {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const transformMatch = (record: any): Match => {
|
export const transformMatch = (record: any, isAdmin: boolean = false): Match => {
|
||||||
return {
|
return {
|
||||||
id: record.id,
|
id: record.id,
|
||||||
order: record.order,
|
order: record.order,
|
||||||
@@ -47,8 +47,8 @@ export const transformMatch = (record: any): Match => {
|
|||||||
is_losers_bracket: record.is_losers_bracket,
|
is_losers_bracket: record.is_losers_bracket,
|
||||||
status: record.status || "tbd",
|
status: record.status || "tbd",
|
||||||
tournament: record.expand?.tournament ? transformTournamentInfo(record.expand?.tournament) : record.tournament,
|
tournament: record.expand?.tournament ? transformTournamentInfo(record.expand?.tournament) : record.tournament,
|
||||||
home: record.expand?.home ? transformTeamInfo(record.expand.home) : record.home,
|
home: record.expand?.home ? (isAdmin ? transformTeam(record.expand.home) : transformTeamInfo(record.expand.home)) : record.home,
|
||||||
away: record.expand?.away ? transformTeamInfo(record.expand.away) : record.away,
|
away: record.expand?.away ? (isAdmin ? transformTeam(record.expand.away) : transformTeamInfo(record.expand.away)) : record.away,
|
||||||
created: record.created,
|
created: record.created,
|
||||||
updated: record.updated,
|
updated: record.updated,
|
||||||
home_seed: record.home_seed,
|
home_seed: record.home_seed,
|
||||||
@@ -135,20 +135,20 @@ export function transformTeam(record: any): Team {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function transformTournament(record: any): Tournament {
|
export function transformTournament(record: any, isAdmin: boolean = false): Tournament {
|
||||||
const teams =
|
const teams =
|
||||||
record.expand?.teams
|
record.expand?.teams
|
||||||
?.sort((a: any, b: any) =>
|
?.sort((a: any, b: any) =>
|
||||||
new Date(a.created) < new Date(b.created) ? -1 : 0
|
new Date(a.created) < new Date(b.created) ? -1 : 0
|
||||||
)
|
)
|
||||||
?.map(transformTeamInfo) ?? [];
|
?.map(isAdmin ? transformTeam : transformTeamInfo) ?? [];
|
||||||
|
|
||||||
const matches =
|
const matches =
|
||||||
record.expand?.matches
|
record.expand?.matches
|
||||||
?.sort((a: any, b: any) =>
|
?.sort((a: any, b: any) =>
|
||||||
a.lid - b.lid ? -1 : 0
|
a.lid - b.lid ? -1 : 0
|
||||||
)
|
)
|
||||||
?.map(transformMatch) ?? [];
|
?.map((match: any) => transformMatch(match, isAdmin)) ?? [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: record.id,
|
id: record.id,
|
||||||
|
|||||||
@@ -96,6 +96,17 @@ export class SpotifyWebApiClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async playTrack(trackId: string, deviceId?: string, positionMs?: number): Promise<void> {
|
||||||
|
const endpoint = deviceId ? `/me/player/play?device_id=${deviceId}` : '/me/player/play';
|
||||||
|
await this.request(endpoint, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({
|
||||||
|
uris: [`spotify:track:${trackId}`],
|
||||||
|
position_ms: positionMs || 0,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async pause(): Promise<void> {
|
async pause(): Promise<void> {
|
||||||
await this.request('/me/player/pause', {
|
await this.request('/me/player/pause', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const useSpotifyPlayback = () => {
|
|||||||
playbackState,
|
playbackState,
|
||||||
currentTrack,
|
currentTrack,
|
||||||
play,
|
play,
|
||||||
|
playTrack,
|
||||||
pause,
|
pause,
|
||||||
skipNext,
|
skipNext,
|
||||||
skipPrevious,
|
skipPrevious,
|
||||||
@@ -29,11 +30,12 @@ export const useSpotifyPlayback = () => {
|
|||||||
refreshPlaybackState,
|
refreshPlaybackState,
|
||||||
isLoading,
|
isLoading,
|
||||||
} = useSpotify();
|
} = useSpotify();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
playbackState,
|
playbackState,
|
||||||
currentTrack,
|
currentTrack,
|
||||||
play,
|
play,
|
||||||
|
playTrack,
|
||||||
pause,
|
pause,
|
||||||
skipNext,
|
skipNext,
|
||||||
skipPrevious,
|
skipPrevious,
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export interface SpotifyContextType extends SpotifyAuthState {
|
|||||||
logout: () => void;
|
logout: () => void;
|
||||||
|
|
||||||
play: (deviceId?: string) => Promise<void>;
|
play: (deviceId?: string) => Promise<void>;
|
||||||
|
playTrack: (trackId: string, deviceId?: string, positionMs?: number) => Promise<void>;
|
||||||
pause: () => Promise<void>;
|
pause: () => Promise<void>;
|
||||||
skipNext: () => Promise<void>;
|
skipNext: () => Promise<void>;
|
||||||
skipPrevious: () => Promise<void>;
|
skipPrevious: () => Promise<void>;
|
||||||
|
|||||||
Reference in New Issue
Block a user