Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e7bb991df7 | |||
| 92ab7d572c | |||
| 795a6b7290 | |||
| d010d68056 | |||
| 8ab927333b |
@@ -1 +0,0 @@
|
|||||||
[]
|
|
||||||
@@ -61,49 +61,48 @@ export class FavoritesStore {
|
|||||||
return this.favorites;
|
return this.favorites;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addFavorite(item: PlaylistItem, fetchMetadata: boolean = true): Promise<void> {
|
async addFavorite(filename: string): Promise<void> {
|
||||||
// Check if the item already exists by filename
|
// Check if the item already exists by filename
|
||||||
const exists = this.favorites.some(f => f.filename === item.filename);
|
const exists = this.favorites.some(f => f.filename === filename);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
this.favorites.push({
|
this.favorites.push({
|
||||||
...item,
|
filename: filename,
|
||||||
id: this.favorites.length // Generate new ID
|
id: this.favorites.length // Generate new ID
|
||||||
});
|
});
|
||||||
await this.saveFavorites();
|
await this.saveFavorites();
|
||||||
} else {
|
|
||||||
// Otherwise, update the item with the new metadata
|
|
||||||
const index = this.favorites.findIndex(f => f.filename === item.filename);
|
|
||||||
this.favorites[index] = {
|
|
||||||
...this.favorites[index],
|
|
||||||
...item,
|
|
||||||
};
|
|
||||||
await this.saveFavorites();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the item is missing metadata, fetch it
|
// Fetch metadata for the new favorite
|
||||||
if (fetchMetadata && !item.metadata) {
|
await this.fetchMetadata(filename);
|
||||||
await this.fetchMetadata(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchMetadata(item: PlaylistItem): Promise<void> {
|
private async fetchMetadata(filename: string): Promise<void> {
|
||||||
console.log("Fetching metadata for " + item.filename);
|
console.log("Fetching metadata for " + filename);
|
||||||
const metadata = await getLinkPreview(item.filename);
|
const metadata = await getLinkPreview(filename);
|
||||||
|
|
||||||
item.metadata = {
|
const item: PlaylistItem = {
|
||||||
|
filename: filename,
|
||||||
|
id: this.favorites.length,
|
||||||
|
metadata: {
|
||||||
title: (metadata as any)?.title,
|
title: (metadata as any)?.title,
|
||||||
description: (metadata as any)?.description,
|
description: (metadata as any)?.description,
|
||||||
siteName: (metadata as any)?.siteName,
|
siteName: (metadata as any)?.siteName,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Metadata fetched for " + item.filename);
|
console.log("Metadata fetched for " + item.filename);
|
||||||
console.log(item);
|
console.log(item);
|
||||||
|
|
||||||
await this.addFavorite(item, false);
|
const index = this.favorites.findIndex(f => f.filename === filename);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.favorites[index] = item;
|
||||||
|
await this.saveFavorites();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeFavorite(id: number): Promise<void> {
|
async removeFavorite(filename: string): Promise<void> {
|
||||||
this.favorites = this.favorites.filter(f => f.id !== id);
|
console.log("Removing favorite " + filename);
|
||||||
|
this.favorites = this.favorites.filter(f => f.filename !== filename);
|
||||||
await this.saveFavorites();
|
await this.saveFavorites();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,10 +33,12 @@ export class MediaPlayer {
|
|||||||
constructor() {
|
constructor() {
|
||||||
const socketFilename = Math.random().toString(36).substring(2, 10);
|
const socketFilename = Math.random().toString(36).substring(2, 10);
|
||||||
const socketPath = `/tmp/mpv-${socketFilename}`;
|
const socketPath = `/tmp/mpv-${socketFilename}`;
|
||||||
|
const enableVideo = process.env.ENABLE_VIDEO || false;
|
||||||
|
|
||||||
console.log("Starting player process");
|
console.log("Starting player process (video: " + (enableVideo ? "enabled" : "disabled") + ")");
|
||||||
this.playerProcess = spawn("mpv", [
|
this.playerProcess = spawn("mpv", [
|
||||||
"--no-video",
|
"--video=" + (enableVideo ? "auto" : "no"),
|
||||||
|
"--fullscreen",
|
||||||
"--no-terminal",
|
"--no-terminal",
|
||||||
"--idle=yes",
|
"--idle=yes",
|
||||||
"--input-ipc-server=" + socketPath
|
"--input-ipc-server=" + socketPath
|
||||||
@@ -172,12 +174,12 @@ export class MediaPlayer {
|
|||||||
return this.favoritesStore.getFavorites();
|
return this.favoritesStore.getFavorites();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async addFavorite(item: PlaylistItem) {
|
public async addFavorite(filename: string) {
|
||||||
return this.favoritesStore.addFavorite(item);
|
return this.favoritesStore.addFavorite(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async removeFavorite(id: number) {
|
public async removeFavorite(filename: string) {
|
||||||
return this.favoritesStore.removeFavorite(id);
|
return this.favoritesStore.removeFavorite(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async clearFavorites() {
|
public async clearFavorites() {
|
||||||
|
|||||||
@@ -140,14 +140,15 @@ apiRouter.get("/favorites", withErrorHandling(async (req, res) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
apiRouter.post("/favorites", withErrorHandling(async (req, res) => {
|
apiRouter.post("/favorites", withErrorHandling(async (req, res) => {
|
||||||
const item = req.body as PlaylistItem;
|
const { filename } = req.body as { filename: string };
|
||||||
await mediaPlayer.addFavorite(item);
|
console.log("Adding favorite: " + filename);
|
||||||
|
await mediaPlayer.addFavorite(filename);
|
||||||
res.send(JSON.stringify({ success: true }));
|
res.send(JSON.stringify({ success: true }));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
apiRouter.delete("/favorites/:id", withErrorHandling(async (req, res) => {
|
apiRouter.delete("/favorites/:filename", withErrorHandling(async (req, res) => {
|
||||||
const { id } = req.params;
|
const { filename } = req.params as { filename: string };
|
||||||
await mediaPlayer.removeFavorite(parseInt(id));
|
await mediaPlayer.removeFavorite(filename);
|
||||||
res.send(JSON.stringify({ success: true }));
|
res.send(JSON.stringify({ success: true }));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -142,28 +142,18 @@ export const API = {
|
|||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
async addToFavorites(url: string): Promise<void> {
|
async addToFavorites(filename: string): Promise<void> {
|
||||||
// Maybe a little weird to make an empty PlaylistItem here, but we do want to support adding
|
|
||||||
// known PlaylistItems to favorites in the future.
|
|
||||||
const playlistItem: PlaylistItem = {
|
|
||||||
filename: url,
|
|
||||||
title: null,
|
|
||||||
id: 0,
|
|
||||||
playing: null,
|
|
||||||
metadata: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
await fetch('/api/favorites', {
|
await fetch('/api/favorites', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify(playlistItem),
|
body: JSON.stringify({ filename }),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async removeFromFavorites(id: number): Promise<void> {
|
async removeFromFavorites(filename: string): Promise<void> {
|
||||||
await fetch(`/api/favorites/${id}`, { method: 'DELETE' });
|
await fetch(`/api/favorites/${encodeURIComponent(filename)}`, { method: 'DELETE' });
|
||||||
},
|
},
|
||||||
|
|
||||||
async clearFavorites(): Promise<void> {
|
async clearFavorites(): Promise<void> {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useState, KeyboardEvent, ChangeEvent } from 'react';
|
|||||||
import { FaSearch } from 'react-icons/fa';
|
import { FaSearch } from 'react-icons/fa';
|
||||||
import InvidiousSearchModal from './InvidiousSearchModal';
|
import InvidiousSearchModal from './InvidiousSearchModal';
|
||||||
import { USE_INVIDIOUS } from '../config';
|
import { USE_INVIDIOUS } from '../config';
|
||||||
|
|
||||||
interface AddSongPanelProps {
|
interface AddSongPanelProps {
|
||||||
onAddURL: (url: string) => void;
|
onAddURL: (url: string) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback, ReactNode } from 'react';
|
||||||
import SongTable from './SongTable';
|
import SongTable from './SongTable';
|
||||||
import NowPlaying from './NowPlaying';
|
import NowPlaying from './NowPlaying';
|
||||||
import AddSongPanel from './AddSongPanel';
|
import AddSongPanel from './AddSongPanel';
|
||||||
import { TabView, Tab } from './TabView';
|
import { TabView, Tab } from './TabView';
|
||||||
import { API, getDisplayTitle, PlaylistItem, ServerEvent } from '../api/player';
|
import { API, getDisplayTitle, PlaylistItem, ServerEvent } from '../api/player';
|
||||||
import { FaMusic, FaHeart } from 'react-icons/fa';
|
import { FaMusic, FaHeart, FaPlus } from 'react-icons/fa';
|
||||||
import useWebSocket from 'react-use-websocket';
|
import useWebSocket from 'react-use-websocket';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
enum Tabs {
|
enum Tabs {
|
||||||
Playlist = "playlist",
|
Playlist = "playlist",
|
||||||
@@ -21,10 +22,11 @@ const EmptyContent: React.FC<{ label: string}> = ({label}) => (
|
|||||||
interface SonglistContentProps {
|
interface SonglistContentProps {
|
||||||
songs: PlaylistItem[];
|
songs: PlaylistItem[];
|
||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
|
auxControlProvider?: (song: PlaylistItem) => ReactNode;
|
||||||
onNeedsRefresh: () => void;
|
onNeedsRefresh: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PlaylistContent: React.FC<SonglistContentProps> = ({ songs, isPlaying, onNeedsRefresh }) => {
|
const PlaylistContent: React.FC<SonglistContentProps> = ({ songs, isPlaying, auxControlProvider, onNeedsRefresh }) => {
|
||||||
const handleDelete = (index: number) => {
|
const handleDelete = (index: number) => {
|
||||||
API.removeFromPlaylist(index);
|
API.removeFromPlaylist(index);
|
||||||
onNeedsRefresh();
|
onNeedsRefresh();
|
||||||
@@ -40,6 +42,7 @@ const PlaylistContent: React.FC<SonglistContentProps> = ({ songs, isPlaying, onN
|
|||||||
<SongTable
|
<SongTable
|
||||||
songs={songs}
|
songs={songs}
|
||||||
isPlaying={isPlaying}
|
isPlaying={isPlaying}
|
||||||
|
auxControlProvider={auxControlProvider}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onSkipTo={handleSkipTo}
|
onSkipTo={handleSkipTo}
|
||||||
/>
|
/>
|
||||||
@@ -49,9 +52,9 @@ const PlaylistContent: React.FC<SonglistContentProps> = ({ songs, isPlaying, onN
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const FavoritesContent: React.FC<SonglistContentProps> = ({ songs, isPlaying, onNeedsRefresh }) => {
|
const FavoritesContent: React.FC<SonglistContentProps> = ({ songs, isPlaying, auxControlProvider, onNeedsRefresh }) => {
|
||||||
const handleDelete = (index: number) => {
|
const handleDelete = (index: number) => {
|
||||||
API.removeFromFavorites(index);
|
API.removeFromFavorites(songs[index].filename);
|
||||||
onNeedsRefresh();
|
onNeedsRefresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -66,6 +69,7 @@ const FavoritesContent: React.FC<SonglistContentProps> = ({ songs, isPlaying, on
|
|||||||
<SongTable
|
<SongTable
|
||||||
songs={songs}
|
songs={songs}
|
||||||
isPlaying={isPlaying}
|
isPlaying={isPlaying}
|
||||||
|
auxControlProvider={auxControlProvider}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onSkipTo={handleSkipTo}
|
onSkipTo={handleSkipTo}
|
||||||
/>
|
/>
|
||||||
@@ -215,6 +219,62 @@ const App: React.FC = () => {
|
|||||||
fetchFavorites();
|
fetchFavorites();
|
||||||
}, [fetchPlaylist, fetchNowPlaying, fetchFavorites]);
|
}, [fetchPlaylist, fetchNowPlaying, fetchFavorites]);
|
||||||
|
|
||||||
|
const AuxButton: React.FC<{ children: ReactNode, className: string, title: string, onClick: () => void }> = (props) => (
|
||||||
|
<button
|
||||||
|
className={
|
||||||
|
classNames("hover:text-white transition-colors px-3 py-1 rounded", props.className)
|
||||||
|
}
|
||||||
|
title={props.title}
|
||||||
|
onClick={props.onClick}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
const playlistAuxControlProvider = (song: PlaylistItem) => {
|
||||||
|
const isFavorite = favorites.some(f => f.filename === song.filename);
|
||||||
|
return (
|
||||||
|
<AuxButton
|
||||||
|
className={classNames({
|
||||||
|
"text-red-500": isFavorite,
|
||||||
|
"text-white/40": !isFavorite,
|
||||||
|
})}
|
||||||
|
title={isFavorite ? "Remove from favorites" : "Add to favorites"}
|
||||||
|
onClick={() => {
|
||||||
|
if (isFavorite) {
|
||||||
|
API.removeFromFavorites(song.filename);
|
||||||
|
} else {
|
||||||
|
API.addToFavorites(song.filename);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaHeart />
|
||||||
|
</AuxButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const favoritesAuxControlProvider = (song: PlaylistItem) => {
|
||||||
|
const isInPlaylist = playlist.some(p => p.filename === song.filename);
|
||||||
|
return (
|
||||||
|
<AuxButton
|
||||||
|
className={classNames({
|
||||||
|
"text-white/40": isInPlaylist,
|
||||||
|
"text-white": !isInPlaylist,
|
||||||
|
})}
|
||||||
|
title={isInPlaylist ? "Remove from playlist" : "Add to playlist"}
|
||||||
|
onClick={() => {
|
||||||
|
if (isInPlaylist) {
|
||||||
|
API.removeFromPlaylist(playlist.findIndex(p => p.filename === song.filename));
|
||||||
|
} else {
|
||||||
|
API.addToPlaylist(song.filename);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaPlus />
|
||||||
|
</AuxButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-screen w-screen bg-black md:py-10">
|
<div className="flex items-center justify-center h-screen w-screen bg-black md:py-10">
|
||||||
<div className="bg-violet-900 w-full md:max-w-2xl h-full md:max-h-xl md:border md:rounded-2xl flex flex-col">
|
<div className="bg-violet-900 w-full md:max-w-2xl h-full md:max-h-xl md:border md:rounded-2xl flex flex-col">
|
||||||
@@ -238,6 +298,7 @@ const App: React.FC = () => {
|
|||||||
songs={playlist}
|
songs={playlist}
|
||||||
isPlaying={isPlaying}
|
isPlaying={isPlaying}
|
||||||
onNeedsRefresh={refreshContent}
|
onNeedsRefresh={refreshContent}
|
||||||
|
auxControlProvider={playlistAuxControlProvider}
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab label="Favorites" identifier={Tabs.Favorites} icon={<FaHeart />}>
|
<Tab label="Favorites" identifier={Tabs.Favorites} icon={<FaHeart />}>
|
||||||
@@ -245,6 +306,7 @@ const App: React.FC = () => {
|
|||||||
songs={favorites.map(f => ({ ...f, playing: f.filename === nowPlayingFileName }))}
|
songs={favorites.map(f => ({ ...f, playing: f.filename === nowPlayingFileName }))}
|
||||||
isPlaying={isPlaying}
|
isPlaying={isPlaying}
|
||||||
onNeedsRefresh={refreshContent}
|
onNeedsRefresh={refreshContent}
|
||||||
|
auxControlProvider={favoritesAuxControlProvider}
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
</TabView>
|
</TabView>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect, ReactNode } from 'react';
|
||||||
import { FaPlay, FaVolumeUp, FaVolumeOff } from 'react-icons/fa';
|
import { FaPlay, FaVolumeUp, FaVolumeOff } from 'react-icons/fa';
|
||||||
import { getDisplayTitle, PlaylistItem } from '../api/player';
|
import { getDisplayTitle, PlaylistItem } from '../api/player';
|
||||||
|
|
||||||
@@ -11,12 +11,13 @@ export enum PlayState {
|
|||||||
|
|
||||||
export interface SongRowProps {
|
export interface SongRowProps {
|
||||||
song: PlaylistItem;
|
song: PlaylistItem;
|
||||||
|
auxControl?: ReactNode;
|
||||||
playState: PlayState;
|
playState: PlayState;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onPlay: () => void;
|
onPlay: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SongRow: React.FC<SongRowProps> = ({ song, playState, onDelete, onPlay }) => {
|
const SongRow: React.FC<SongRowProps> = ({ song, auxControl, playState, onDelete, onPlay }) => {
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
@@ -39,7 +40,8 @@ const SongRow: React.FC<SongRowProps> = ({ song, playState, onDelete, onPlay })
|
|||||||
const displayTitle = getDisplayTitle(song);
|
const displayTitle = getDisplayTitle(song);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames("flex flex-row w-full h-24 px-2 py-5 items-center border-b gap-2 transition-colors", {
|
<div className={classNames(
|
||||||
|
"flex flex-row w-full h-20 px-2 py-2 items-center border-b gap-2 transition-colors shrink-0", {
|
||||||
"qc-highlighted": (playState === PlayState.Playing || playState === PlayState.Paused),
|
"qc-highlighted": (playState === PlayState.Playing || playState === PlayState.Paused),
|
||||||
"bg-black/30": playState === PlayState.NotPlaying,
|
"bg-black/30": playState === PlayState.NotPlaying,
|
||||||
})}>
|
})}>
|
||||||
@@ -76,6 +78,8 @@ const SongRow: React.FC<SongRowProps> = ({ song, playState, onDelete, onPlay })
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
|
{auxControl}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
className="text-red-100 px-3 py-1 bg-red-500/40 rounded"
|
className="text-red-100 px-3 py-1 bg-red-500/40 rounded"
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef, ReactNode } from "react";
|
||||||
import SongRow, { PlayState } from "./SongRow";
|
import SongRow, { PlayState } from "./SongRow";
|
||||||
import { PlaylistItem } from "../api/player";
|
import { PlaylistItem } from "../api/player";
|
||||||
|
|
||||||
interface SongTableProps {
|
interface SongTableProps {
|
||||||
songs: PlaylistItem[];
|
songs: PlaylistItem[];
|
||||||
isPlaying: boolean;
|
isPlaying: boolean
|
||||||
|
auxControlProvider?: (song: PlaylistItem) => ReactNode;
|
||||||
onDelete: (index: number) => void;
|
onDelete: (index: number) => void;
|
||||||
onSkipTo: (index: number) => void;
|
onSkipTo: (index: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SongTable: React.FC<SongTableProps> = ({ songs, isPlaying, onDelete, onSkipTo }) => {
|
const SongTable: React.FC<SongTableProps> = ({ songs, isPlaying, auxControlProvider, onDelete, onSkipTo }) => {
|
||||||
const nowPlayingIndex = songs.findIndex(song => song.playing ?? false);
|
const nowPlayingIndex = songs.findIndex(song => song.playing ?? false);
|
||||||
const songTableRef = useRef<HTMLDivElement>(null);
|
const songTableRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -21,11 +22,12 @@ const SongTable: React.FC<SongTableProps> = ({ songs, isPlaying, onDelete, onSki
|
|||||||
}, [nowPlayingIndex]);
|
}, [nowPlayingIndex]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full h-full overflow-y-auto border-y" ref={songTableRef}>
|
<div className="flex flex-col w-full h-full overflow-y-auto" ref={songTableRef}>
|
||||||
{songs.map((song, index) => (
|
{songs.map((song, index) => (
|
||||||
<SongRow
|
<SongRow
|
||||||
key={index}
|
key={index}
|
||||||
song={song}
|
song={song}
|
||||||
|
auxControl={auxControlProvider ? auxControlProvider(song) : undefined}
|
||||||
playState={
|
playState={
|
||||||
(song.playing ?? false) ? (isPlaying ? PlayState.Playing : PlayState.Paused)
|
(song.playing ?? false) ? (isPlaying ? PlayState.Playing : PlayState.Paused)
|
||||||
: PlayState.NotPlaying
|
: PlayState.NotPlaying
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const TabView = <T,>({ children, selectedTab, onTabChange }: TabViewProps
|
|||||||
) as React.ReactElement<TabProps<T>>[];
|
) as React.ReactElement<TabProps<T>>[];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col flex-1 overflow-hidden">
|
||||||
<div className="flex flex-row h-11 border-b border-white/20">
|
<div className="flex flex-row h-11 border-b border-white/20">
|
||||||
{tabs.map((tab, index) => {
|
{tabs.map((tab, index) => {
|
||||||
const isSelected = selectedTab === tab.props.identifier;
|
const isSelected = selectedTab === tab.props.identifier;
|
||||||
@@ -47,7 +47,7 @@ export const TabView = <T,>({ children, selectedTab, onTabChange }: TabViewProps
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1 overflow-hidden">
|
||||||
{tabs.map((tab, index) => (
|
{tabs.map((tab, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
|
|||||||
Reference in New Issue
Block a user