Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
fa2532503c
commit
c1fd104be3
@ -1,102 +1,199 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { IonToggle, IonItem, IonLabel, IonList } from '@ionic/react';
|
import { IonContent, IonHeader, IonTitle, IonToolbar, IonList, IonItem, IonLabel, IonToggle, IonButton, IonIcon, IonModal, IonSearchbar } from '@ionic/react';
|
||||||
import { } from '../../components/common';
|
import { ban, trash } from 'ionicons/icons';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectControllerName } from '../../redux';
|
import { selectIsAdmin, selectSettings } from '../../redux';
|
||||||
import { settingsService } from '../../firebase/services';
|
import { useDisabledSongs } from '../../hooks';
|
||||||
import { useToast } from '../../hooks';
|
import { InfiniteScrollList, ActionButton } from '../../components/common';
|
||||||
|
import { filterSongs } from '../../utils/dataProcessing';
|
||||||
|
import type { Song } from '../../types';
|
||||||
|
|
||||||
interface PlayerSettings {
|
interface DisabledSongDisplay {
|
||||||
autoadvance: boolean;
|
key?: string;
|
||||||
userpick: boolean;
|
path: string;
|
||||||
|
artist: string;
|
||||||
|
title: string;
|
||||||
|
disabledAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Settings: React.FC = () => {
|
const Settings: React.FC = () => {
|
||||||
const [settings, setSettings] = useState<PlayerSettings>({
|
const isAdmin = useAppSelector(selectIsAdmin);
|
||||||
autoadvance: false,
|
const playerSettings = useAppSelector(selectSettings);
|
||||||
userpick: false
|
const {
|
||||||
});
|
disabledSongs,
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
loading,
|
||||||
|
removeDisabledSong
|
||||||
|
} = useDisabledSongs();
|
||||||
|
|
||||||
const controllerName = useAppSelector(selectControllerName);
|
const [showDisabledSongsModal, setShowDisabledSongsModal] = useState(false);
|
||||||
const { showSuccess, showError } = useToast();
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
// Load settings on mount
|
// Convert disabled songs object to array for display
|
||||||
useEffect(() => {
|
const disabledSongsArray: DisabledSongDisplay[] = Object.entries(disabledSongs).map(([key, disabledSong]) => ({
|
||||||
if (controllerName) {
|
key: disabledSong.key || key,
|
||||||
loadSettings();
|
path: disabledSong.path,
|
||||||
}
|
artist: disabledSong.artist,
|
||||||
}, [controllerName]);
|
title: disabledSong.title,
|
||||||
|
disabledAt: disabledSong.disabledAt,
|
||||||
|
}));
|
||||||
|
|
||||||
const loadSettings = async () => {
|
// Filter disabled songs by search term
|
||||||
if (!controllerName) return;
|
const filteredDisabledSongs: DisabledSongDisplay[] = searchTerm.trim()
|
||||||
|
? filterSongs(disabledSongsArray, searchTerm) as DisabledSongDisplay[]
|
||||||
try {
|
: disabledSongsArray;
|
||||||
setIsLoading(true);
|
|
||||||
const currentSettings = await settingsService.getSettings(controllerName);
|
const handleToggleSetting = async (setting: string, value: boolean) => {
|
||||||
if (currentSettings) {
|
// This would need to be implemented with the settings service
|
||||||
setSettings(currentSettings);
|
console.log(`Toggle ${setting} to ${value}`);
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load settings:', error);
|
|
||||||
showError('Failed to load settings');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSettingChange = async (setting: keyof PlayerSettings, value: boolean) => {
|
const handleRemoveDisabledSong = async (song: DisabledSongDisplay) => {
|
||||||
if (!controllerName) return;
|
// Create a minimal song object with the path for removal
|
||||||
|
const songForRemoval: Song = {
|
||||||
try {
|
path: song.path,
|
||||||
await settingsService.updateSetting(controllerName, setting, value);
|
artist: song.artist,
|
||||||
setSettings(prev => ({ ...prev, [setting]: value }));
|
title: song.title,
|
||||||
showSuccess(`${setting === 'autoadvance' ? 'Auto-advance' : 'User pick'} setting updated`);
|
key: song.key,
|
||||||
} catch (error) {
|
};
|
||||||
console.error('Failed to update setting:', error);
|
await removeDisabledSong(songForRemoval);
|
||||||
showError('Failed to update setting');
|
|
||||||
// Revert the change on error
|
|
||||||
setSettings(prev => ({ ...prev, [setting]: !value }));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto p-6 text-center">
|
||||||
|
<p className="text-gray-500">Admin access required to view settings.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="text-sm text-gray-500 text-center mb-4">
|
<div className="max-w-4xl mx-auto p-6 settings-container">
|
||||||
Configure player behavior
|
{/* Player Settings */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<h2 className="text-large font-semibold mb-4">Player Settings</h2>
|
||||||
|
<IonList className="rounded-lg overflow-hidden">
|
||||||
|
<IonItem>
|
||||||
|
<IonLabel>Auto Advance</IonLabel>
|
||||||
|
<IonToggle
|
||||||
|
slot="end"
|
||||||
|
checked={playerSettings?.autoadvance || false}
|
||||||
|
onIonChange={(e) => handleToggleSetting('autoadvance', e.detail.checked)}
|
||||||
|
/>
|
||||||
|
</IonItem>
|
||||||
|
<IonItem>
|
||||||
|
<IonLabel>User Pick</IonLabel>
|
||||||
|
<IonToggle
|
||||||
|
slot="end"
|
||||||
|
checked={playerSettings?.userpick || false}
|
||||||
|
onIonChange={(e) => handleToggleSetting('userpick', e.detail.checked)}
|
||||||
|
/>
|
||||||
|
</IonItem>
|
||||||
|
</IonList>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Disabled Songs Management */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-lg font-semibold">Disabled Songs Management</h2>
|
||||||
|
<IonButton
|
||||||
|
fill="outline"
|
||||||
|
onClick={() => setShowDisabledSongsModal(true)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<IonIcon icon={ban} slot="start" />
|
||||||
|
Manage Disabled Songs ({disabledSongsArray.length})
|
||||||
|
</IonButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-50 rounded-lg p-4">
|
||||||
|
<p className="text-sm text-gray-600 mb-2">
|
||||||
|
Songs marked as disabled will be hidden from search results, favorites, and other song lists.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Use the search page to mark individual songs as disabled, or manage all disabled songs here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-4xl mx-auto p-6">
|
{/* Disabled Songs Modal */}
|
||||||
<IonList>
|
<IonModal
|
||||||
<IonItem>
|
isOpen={showDisabledSongsModal}
|
||||||
<IonLabel>
|
onDidDismiss={() => setShowDisabledSongsModal(false)}
|
||||||
<h3>Auto-advance Queue</h3>
|
breakpoints={[0, 0.5, 0.8]}
|
||||||
<p>Automatically advance to the next song when the current song finishes</p>
|
initialBreakpoint={0.8}
|
||||||
</IonLabel>
|
>
|
||||||
<IonToggle
|
<IonHeader>
|
||||||
slot="end"
|
<IonToolbar>
|
||||||
checked={settings.autoadvance}
|
<IonTitle>Disabled Songs ({filteredDisabledSongs.length})</IonTitle>
|
||||||
onIonChange={(e) => handleSettingChange('autoadvance', e.detail.checked)}
|
<IonButton
|
||||||
disabled={isLoading}
|
slot="end"
|
||||||
/>
|
fill="clear"
|
||||||
</IonItem>
|
onClick={() => setShowDisabledSongsModal(false)}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</IonButton>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
|
||||||
<IonItem>
|
<IonContent>
|
||||||
<IonLabel>
|
<div className="p-4">
|
||||||
<h3>User Pick Mode</h3>
|
{/* Search */}
|
||||||
<p>Allow users to pick their own songs from the queue</p>
|
<IonSearchbar
|
||||||
</IonLabel>
|
placeholder="Search disabled songs..."
|
||||||
<IonToggle
|
value={searchTerm}
|
||||||
slot="end"
|
onIonInput={(e) => setSearchTerm(e.detail.value || '')}
|
||||||
checked={settings.userpick}
|
debounce={300}
|
||||||
onIonChange={(e) => handleSettingChange('userpick', e.detail.checked)}
|
showClearButton="focus"
|
||||||
disabled={isLoading}
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Disabled Songs List */}
|
||||||
|
<InfiniteScrollList<DisabledSongDisplay>
|
||||||
|
items={filteredDisabledSongs}
|
||||||
|
isLoading={loading}
|
||||||
|
hasMore={false}
|
||||||
|
onLoadMore={() => {}}
|
||||||
|
renderItem={(song) => (
|
||||||
|
<IonItem>
|
||||||
|
<IonLabel>
|
||||||
|
<h3 className="text-sm font-medium text-gray-900">
|
||||||
|
{song.title || 'Unknown Title'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{song.artist || 'Unknown Artist'}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400 break-words">
|
||||||
|
{song.path}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400">
|
||||||
|
Disabled: {new Date(song.disabledAt || '').toLocaleDateString()}
|
||||||
|
</p>
|
||||||
|
</IonLabel>
|
||||||
|
|
||||||
|
<div slot="end" className="flex items-center gap-2 ml-2">
|
||||||
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
|
<ActionButton
|
||||||
|
onClick={() => handleRemoveDisabledSong(song)}
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<IonIcon icon={trash} />
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</IonItem>
|
||||||
|
)}
|
||||||
|
emptyTitle="No disabled songs"
|
||||||
|
emptyMessage="Songs marked as disabled will appear here"
|
||||||
|
loadingTitle="Loading disabled songs..."
|
||||||
|
loadingMessage="Please wait while disabled songs are being loaded"
|
||||||
/>
|
/>
|
||||||
</IonItem>
|
</IonContent>
|
||||||
</IonList>
|
</IonModal>
|
||||||
</div>
|
</>
|
||||||
</>
|
);
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Settings;
|
export default Settings;
|
||||||
@ -9,7 +9,7 @@ import {
|
|||||||
update
|
update
|
||||||
} from 'firebase/database';
|
} from 'firebase/database';
|
||||||
import { database } from './config';
|
import { database } from './config';
|
||||||
import type { Song, QueueItem, Controller, Singer } from '../types';
|
import type { Song, QueueItem, Controller, Singer, DisabledSong } from '../types';
|
||||||
|
|
||||||
// Basic CRUD operations for controllers
|
// Basic CRUD operations for controllers
|
||||||
export const controllerService = {
|
export const controllerService = {
|
||||||
@ -318,4 +318,90 @@ export const settingsService = {
|
|||||||
|
|
||||||
return () => off(settingsRef);
|
return () => off(settingsRef);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Disabled songs management operations
|
||||||
|
export const disabledSongsService = {
|
||||||
|
// Generate a hash for the song path to use as a Firebase-safe key
|
||||||
|
generateSongKey: (songPath: string): string => {
|
||||||
|
// Simple hash function for the path
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < songPath.length; i++) {
|
||||||
|
const char = songPath.charCodeAt(i);
|
||||||
|
hash = ((hash << 5) - hash) + char;
|
||||||
|
hash = hash & hash; // Convert to 32-bit integer
|
||||||
|
}
|
||||||
|
return Math.abs(hash).toString(36); // Convert to base36 for shorter keys
|
||||||
|
},
|
||||||
|
|
||||||
|
// Add a song to the disabled list
|
||||||
|
addDisabledSong: async (controllerName: string, song: Song) => {
|
||||||
|
console.log('disabledSongsService.addDisabledSong called with:', { controllerName, song });
|
||||||
|
|
||||||
|
if (!controllerName) {
|
||||||
|
throw new Error('Controller name is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!song.path) {
|
||||||
|
throw new Error('Song path is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!song.artist || !song.title) {
|
||||||
|
throw new Error('Song artist and title are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const songKey = disabledSongsService.generateSongKey(song.path);
|
||||||
|
console.log('Generated song key:', songKey);
|
||||||
|
|
||||||
|
const disabledSongRef = ref(database, `controllers/${controllerName}/disabledSongs/${songKey}`);
|
||||||
|
const disabledSong = {
|
||||||
|
path: song.path,
|
||||||
|
artist: song.artist,
|
||||||
|
title: song.title,
|
||||||
|
key: song.key,
|
||||||
|
disabledAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Saving disabled song:', disabledSong);
|
||||||
|
await set(disabledSongRef, disabledSong);
|
||||||
|
console.log('Disabled song saved successfully');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Remove a song from the disabled list
|
||||||
|
removeDisabledSong: async (controllerName: string, songPath: string) => {
|
||||||
|
const songKey = disabledSongsService.generateSongKey(songPath);
|
||||||
|
const disabledSongRef = ref(database, `controllers/${controllerName}/disabledSongs/${songKey}`);
|
||||||
|
await remove(disabledSongRef);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Check if a song is disabled
|
||||||
|
isSongDisabled: async (controllerName: string, songPath: string): Promise<boolean> => {
|
||||||
|
const songKey = disabledSongsService.generateSongKey(songPath);
|
||||||
|
const disabledSongRef = ref(database, `controllers/${controllerName}/disabledSongs/${songKey}`);
|
||||||
|
const snapshot = await get(disabledSongRef);
|
||||||
|
return snapshot.exists();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get all disabled songs
|
||||||
|
getDisabledSongs: async (controllerName: string) => {
|
||||||
|
const disabledSongsRef = ref(database, `controllers/${controllerName}/disabledSongs`);
|
||||||
|
const snapshot = await get(disabledSongsRef);
|
||||||
|
return snapshot.exists() ? snapshot.val() : {};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get disabled song paths as a Set for fast lookup
|
||||||
|
getDisabledSongPaths: async (controllerName: string): Promise<Set<string>> => {
|
||||||
|
const disabledSongs = await disabledSongsService.getDisabledSongs(controllerName);
|
||||||
|
return new Set(Object.values(disabledSongs as Record<string, DisabledSong>).map((song) => song.path));
|
||||||
|
},
|
||||||
|
|
||||||
|
// Listen to disabled songs changes
|
||||||
|
subscribeToDisabledSongs: (controllerName: string, callback: (data: Record<string, DisabledSong>) => void) => {
|
||||||
|
const disabledSongsRef = ref(database, `controllers/${controllerName}/disabledSongs`);
|
||||||
|
onValue(disabledSongsRef, (snapshot) => {
|
||||||
|
callback(snapshot.exists() ? snapshot.val() : {});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => off(disabledSongsRef);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
@ -9,4 +9,5 @@ export { useFavorites } from './useFavorites';
|
|||||||
export { useNewSongs } from './useNewSongs';
|
export { useNewSongs } from './useNewSongs';
|
||||||
export { useArtists } from './useArtists';
|
export { useArtists } from './useArtists';
|
||||||
export { useSingers } from './useSingers';
|
export { useSingers } from './useSingers';
|
||||||
export { useSongLists } from './useSongLists';
|
export { useSongLists } from './useSongLists';
|
||||||
|
export { useDisabledSongs } from './useDisabledSongs';
|
||||||
105
src/hooks/useDisabledSongs.ts
Normal file
105
src/hooks/useDisabledSongs.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { disabledSongsService } from '../firebase/services';
|
||||||
|
import { useAppSelector } from '../redux';
|
||||||
|
import { selectControllerName } from '../redux';
|
||||||
|
import { useToast } from './useToast';
|
||||||
|
import type { Song, DisabledSong } from '../types';
|
||||||
|
|
||||||
|
export const useDisabledSongs = () => {
|
||||||
|
const [disabledSongPaths, setDisabledSongPaths] = useState<Set<string>>(new Set());
|
||||||
|
const [disabledSongs, setDisabledSongs] = useState<Record<string, DisabledSong>>({});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const controllerName = useAppSelector(selectControllerName);
|
||||||
|
const { showSuccess, showError } = useToast();
|
||||||
|
|
||||||
|
// Load disabled songs on mount and subscribe to changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!controllerName) return;
|
||||||
|
|
||||||
|
const loadDisabledSongs = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const songs = await disabledSongsService.getDisabledSongs(controllerName);
|
||||||
|
const paths = await disabledSongsService.getDisabledSongPaths(controllerName);
|
||||||
|
|
||||||
|
setDisabledSongs(songs);
|
||||||
|
setDisabledSongPaths(paths);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading disabled songs:', error);
|
||||||
|
showError('Failed to load disabled songs');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadDisabledSongs();
|
||||||
|
|
||||||
|
// Subscribe to real-time updates
|
||||||
|
const unsubscribe = disabledSongsService.subscribeToDisabledSongs(
|
||||||
|
controllerName,
|
||||||
|
(songs) => {
|
||||||
|
setDisabledSongs(songs);
|
||||||
|
setDisabledSongPaths(new Set(Object.keys(songs).map(key => decodeURIComponent(key))));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
}, [controllerName, showError]);
|
||||||
|
|
||||||
|
// Check if a song is disabled
|
||||||
|
const isSongDisabled = useCallback((song: Song): boolean => {
|
||||||
|
return disabledSongPaths.has(song.path);
|
||||||
|
}, [disabledSongPaths]);
|
||||||
|
|
||||||
|
// Add a song to disabled list
|
||||||
|
const addDisabledSong = useCallback(async (song: Song) => {
|
||||||
|
if (!controllerName) {
|
||||||
|
console.error('No controller name available');
|
||||||
|
showError('No controller name available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!song.path) {
|
||||||
|
console.error('Song has no path:', song);
|
||||||
|
showError('Song has no path');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Adding disabled song:', { controllerName, song });
|
||||||
|
await disabledSongsService.addDisabledSong(controllerName, song);
|
||||||
|
showSuccess('Song marked as disabled');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding disabled song:', error);
|
||||||
|
showError(`Failed to mark song as disabled: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
}, [controllerName, showSuccess, showError]);
|
||||||
|
|
||||||
|
// Remove a song from disabled list
|
||||||
|
const removeDisabledSong = useCallback(async (song: Song) => {
|
||||||
|
if (!controllerName) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await disabledSongsService.removeDisabledSong(controllerName, song.path);
|
||||||
|
showSuccess('Song re-enabled');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error removing disabled song:', error);
|
||||||
|
showError('Failed to re-enable song');
|
||||||
|
}
|
||||||
|
}, [controllerName, showSuccess, showError]);
|
||||||
|
|
||||||
|
// Filter out disabled songs from an array
|
||||||
|
const filterDisabledSongs = useCallback((songs: Song[]): Song[] => {
|
||||||
|
return songs.filter(song => !isSongDisabled(song));
|
||||||
|
}, [isSongDisabled]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
disabledSongPaths,
|
||||||
|
disabledSongs,
|
||||||
|
loading,
|
||||||
|
isSongDisabled,
|
||||||
|
addDisabledSong,
|
||||||
|
removeDisabledSong,
|
||||||
|
filterDisabledSongs,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -2,6 +2,7 @@ import { useCallback, useMemo, useState } from 'react';
|
|||||||
import { useAppSelector, selectFavoritesArray } from '../redux';
|
import { useAppSelector, selectFavoritesArray } from '../redux';
|
||||||
import { useSongOperations } from './useSongOperations';
|
import { useSongOperations } from './useSongOperations';
|
||||||
import { useToast } from './useToast';
|
import { useToast } from './useToast';
|
||||||
|
import { useDisabledSongs } from './useDisabledSongs';
|
||||||
import type { Song } from '../types';
|
import type { Song } from '../types';
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 20;
|
const ITEMS_PER_PAGE = 20;
|
||||||
@ -10,19 +11,21 @@ export const useFavorites = () => {
|
|||||||
const allFavoritesItems = useAppSelector(selectFavoritesArray);
|
const allFavoritesItems = useAppSelector(selectFavoritesArray);
|
||||||
const { addToQueue, toggleFavorite } = useSongOperations();
|
const { addToQueue, toggleFavorite } = useSongOperations();
|
||||||
const { showSuccess, showError } = useToast();
|
const { showSuccess, showError } = useToast();
|
||||||
|
const { filterDisabledSongs, isSongDisabled, addDisabledSong, removeDisabledSong } = useDisabledSongs();
|
||||||
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
// Paginate the favorites items - show all items up to current page
|
// Filter out disabled songs and paginate
|
||||||
const favoritesItems = useMemo(() => {
|
const favoritesItems = useMemo(() => {
|
||||||
|
const filteredItems = filterDisabledSongs(allFavoritesItems);
|
||||||
const endIndex = currentPage * ITEMS_PER_PAGE;
|
const endIndex = currentPage * ITEMS_PER_PAGE;
|
||||||
return allFavoritesItems.slice(0, endIndex);
|
return filteredItems.slice(0, endIndex);
|
||||||
}, [allFavoritesItems, currentPage]);
|
}, [allFavoritesItems, currentPage, filterDisabledSongs]);
|
||||||
|
|
||||||
const hasMore = useMemo(() => {
|
const hasMore = useMemo(() => {
|
||||||
// Only show "hasMore" if there are more items than currently loaded
|
const filteredItems = filterDisabledSongs(allFavoritesItems);
|
||||||
return allFavoritesItems.length > ITEMS_PER_PAGE && favoritesItems.length < allFavoritesItems.length;
|
return filteredItems.length > ITEMS_PER_PAGE && favoritesItems.length < filteredItems.length;
|
||||||
}, [favoritesItems.length, allFavoritesItems.length]);
|
}, [favoritesItems.length, allFavoritesItems.length, filterDisabledSongs]);
|
||||||
|
|
||||||
const loadMore = useCallback(() => {
|
const loadMore = useCallback(() => {
|
||||||
console.log('useFavorites - loadMore called:', { hasMore, currentPage, allFavoritesItemsLength: allFavoritesItems.length });
|
console.log('useFavorites - loadMore called:', { hasMore, currentPage, allFavoritesItemsLength: allFavoritesItems.length });
|
||||||
@ -49,14 +52,25 @@ export const useFavorites = () => {
|
|||||||
}
|
}
|
||||||
}, [toggleFavorite, showSuccess, showError]);
|
}, [toggleFavorite, showSuccess, showError]);
|
||||||
|
|
||||||
|
const handleToggleDisabled = useCallback(async (song: Song) => {
|
||||||
|
try {
|
||||||
|
if (isSongDisabled(song)) {
|
||||||
|
await removeDisabledSong(song);
|
||||||
|
} else {
|
||||||
|
await addDisabledSong(song);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
showError('Failed to update song disabled status');
|
||||||
|
}
|
||||||
|
}, [isSongDisabled, addDisabledSong, removeDisabledSong, showError]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
favoritesItems,
|
favoritesItems,
|
||||||
allFavoritesItems,
|
|
||||||
hasMore,
|
hasMore,
|
||||||
loadMore,
|
loadMore,
|
||||||
currentPage,
|
|
||||||
totalPages: Math.ceil(allFavoritesItems.length / ITEMS_PER_PAGE),
|
|
||||||
handleAddToQueue,
|
handleAddToQueue,
|
||||||
handleToggleFavorite,
|
handleToggleFavorite,
|
||||||
|
handleToggleDisabled,
|
||||||
|
isSongDisabled,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -2,6 +2,7 @@ import { useCallback, useMemo, useState } from 'react';
|
|||||||
import { useAppSelector, selectHistoryArray } from '../redux';
|
import { useAppSelector, selectHistoryArray } from '../redux';
|
||||||
import { useSongOperations } from './useSongOperations';
|
import { useSongOperations } from './useSongOperations';
|
||||||
import { useToast } from './useToast';
|
import { useToast } from './useToast';
|
||||||
|
import { useDisabledSongs } from './useDisabledSongs';
|
||||||
import type { Song } from '../types';
|
import type { Song } from '../types';
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 20;
|
const ITEMS_PER_PAGE = 20;
|
||||||
@ -10,19 +11,21 @@ export const useHistory = () => {
|
|||||||
const allHistoryItems = useAppSelector(selectHistoryArray);
|
const allHistoryItems = useAppSelector(selectHistoryArray);
|
||||||
const { addToQueue, toggleFavorite } = useSongOperations();
|
const { addToQueue, toggleFavorite } = useSongOperations();
|
||||||
const { showSuccess, showError } = useToast();
|
const { showSuccess, showError } = useToast();
|
||||||
|
const { filterDisabledSongs, isSongDisabled, addDisabledSong, removeDisabledSong } = useDisabledSongs();
|
||||||
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
// Paginate the history items - show all items up to current page
|
// Filter out disabled songs and paginate
|
||||||
const historyItems = useMemo(() => {
|
const historyItems = useMemo(() => {
|
||||||
|
const filteredItems = filterDisabledSongs(allHistoryItems);
|
||||||
const endIndex = currentPage * ITEMS_PER_PAGE;
|
const endIndex = currentPage * ITEMS_PER_PAGE;
|
||||||
return allHistoryItems.slice(0, endIndex);
|
return filteredItems.slice(0, endIndex);
|
||||||
}, [allHistoryItems, currentPage]);
|
}, [allHistoryItems, currentPage, filterDisabledSongs]);
|
||||||
|
|
||||||
const hasMore = useMemo(() => {
|
const hasMore = useMemo(() => {
|
||||||
// Only show "hasMore" if there are more items than currently loaded
|
const filteredItems = filterDisabledSongs(allHistoryItems);
|
||||||
return allHistoryItems.length > ITEMS_PER_PAGE && historyItems.length < allHistoryItems.length;
|
return filteredItems.length > ITEMS_PER_PAGE && historyItems.length < filteredItems.length;
|
||||||
}, [historyItems.length, allHistoryItems.length]);
|
}, [historyItems.length, allHistoryItems.length, filterDisabledSongs]);
|
||||||
|
|
||||||
const loadMore = useCallback(() => {
|
const loadMore = useCallback(() => {
|
||||||
console.log('useHistory - loadMore called:', { hasMore, currentPage, allHistoryItemsLength: allHistoryItems.length });
|
console.log('useHistory - loadMore called:', { hasMore, currentPage, allHistoryItemsLength: allHistoryItems.length });
|
||||||
@ -49,14 +52,25 @@ export const useHistory = () => {
|
|||||||
}
|
}
|
||||||
}, [toggleFavorite, showSuccess, showError]);
|
}, [toggleFavorite, showSuccess, showError]);
|
||||||
|
|
||||||
|
const handleToggleDisabled = useCallback(async (song: Song) => {
|
||||||
|
try {
|
||||||
|
if (isSongDisabled(song)) {
|
||||||
|
await removeDisabledSong(song);
|
||||||
|
} else {
|
||||||
|
await addDisabledSong(song);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
showError('Failed to update song disabled status');
|
||||||
|
}
|
||||||
|
}, [isSongDisabled, addDisabledSong, removeDisabledSong, showError]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
historyItems,
|
historyItems,
|
||||||
allHistoryItems,
|
|
||||||
hasMore,
|
hasMore,
|
||||||
loadMore,
|
loadMore,
|
||||||
currentPage,
|
|
||||||
totalPages: Math.ceil(allHistoryItems.length / ITEMS_PER_PAGE),
|
|
||||||
handleAddToQueue,
|
handleAddToQueue,
|
||||||
handleToggleFavorite,
|
handleToggleFavorite,
|
||||||
|
handleToggleDisabled,
|
||||||
|
isSongDisabled,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -2,6 +2,7 @@ import { useCallback, useMemo, useState } from 'react';
|
|||||||
import { useAppSelector, selectNewSongsArray } from '../redux';
|
import { useAppSelector, selectNewSongsArray } from '../redux';
|
||||||
import { useSongOperations } from './useSongOperations';
|
import { useSongOperations } from './useSongOperations';
|
||||||
import { useToast } from './useToast';
|
import { useToast } from './useToast';
|
||||||
|
import { useDisabledSongs } from './useDisabledSongs';
|
||||||
import type { Song } from '../types';
|
import type { Song } from '../types';
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 20;
|
const ITEMS_PER_PAGE = 20;
|
||||||
@ -10,19 +11,21 @@ export const useNewSongs = () => {
|
|||||||
const allNewSongsItems = useAppSelector(selectNewSongsArray);
|
const allNewSongsItems = useAppSelector(selectNewSongsArray);
|
||||||
const { addToQueue, toggleFavorite } = useSongOperations();
|
const { addToQueue, toggleFavorite } = useSongOperations();
|
||||||
const { showSuccess, showError } = useToast();
|
const { showSuccess, showError } = useToast();
|
||||||
|
const { filterDisabledSongs, isSongDisabled, addDisabledSong, removeDisabledSong } = useDisabledSongs();
|
||||||
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
// Paginate the new songs items - show all items up to current page
|
// Filter out disabled songs and paginate
|
||||||
const newSongsItems = useMemo(() => {
|
const newSongsItems = useMemo(() => {
|
||||||
|
const filteredItems = filterDisabledSongs(allNewSongsItems);
|
||||||
const endIndex = currentPage * ITEMS_PER_PAGE;
|
const endIndex = currentPage * ITEMS_PER_PAGE;
|
||||||
return allNewSongsItems.slice(0, endIndex);
|
return filteredItems.slice(0, endIndex);
|
||||||
}, [allNewSongsItems, currentPage]);
|
}, [allNewSongsItems, currentPage, filterDisabledSongs]);
|
||||||
|
|
||||||
const hasMore = useMemo(() => {
|
const hasMore = useMemo(() => {
|
||||||
// Only show "hasMore" if there are more items than currently loaded
|
const filteredItems = filterDisabledSongs(allNewSongsItems);
|
||||||
return allNewSongsItems.length > ITEMS_PER_PAGE && newSongsItems.length < allNewSongsItems.length;
|
return filteredItems.length > ITEMS_PER_PAGE && newSongsItems.length < filteredItems.length;
|
||||||
}, [newSongsItems.length, allNewSongsItems.length]);
|
}, [newSongsItems.length, allNewSongsItems.length, filterDisabledSongs]);
|
||||||
|
|
||||||
const loadMore = useCallback(() => {
|
const loadMore = useCallback(() => {
|
||||||
console.log('useNewSongs - loadMore called:', { hasMore, currentPage, allNewSongsItemsLength: allNewSongsItems.length });
|
console.log('useNewSongs - loadMore called:', { hasMore, currentPage, allNewSongsItemsLength: allNewSongsItems.length });
|
||||||
@ -49,14 +52,25 @@ export const useNewSongs = () => {
|
|||||||
}
|
}
|
||||||
}, [toggleFavorite, showSuccess, showError]);
|
}, [toggleFavorite, showSuccess, showError]);
|
||||||
|
|
||||||
|
const handleToggleDisabled = useCallback(async (song: Song) => {
|
||||||
|
try {
|
||||||
|
if (isSongDisabled(song)) {
|
||||||
|
await removeDisabledSong(song);
|
||||||
|
} else {
|
||||||
|
await addDisabledSong(song);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
showError('Failed to update song disabled status');
|
||||||
|
}
|
||||||
|
}, [isSongDisabled, addDisabledSong, removeDisabledSong, showError]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
newSongsItems,
|
newSongsItems,
|
||||||
allNewSongsItems,
|
|
||||||
hasMore,
|
hasMore,
|
||||||
loadMore,
|
loadMore,
|
||||||
currentPage,
|
|
||||||
totalPages: Math.ceil(allNewSongsItems.length / ITEMS_PER_PAGE),
|
|
||||||
handleAddToQueue,
|
handleAddToQueue,
|
||||||
handleToggleFavorite,
|
handleToggleFavorite,
|
||||||
|
handleToggleDisabled,
|
||||||
|
isSongDisabled,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -2,6 +2,7 @@ import { useState, useCallback, useMemo } from 'react';
|
|||||||
import { useAppSelector, selectSongsArray } from '../redux';
|
import { useAppSelector, selectSongsArray } from '../redux';
|
||||||
import { useSongOperations } from './useSongOperations';
|
import { useSongOperations } from './useSongOperations';
|
||||||
import { useToast } from './useToast';
|
import { useToast } from './useToast';
|
||||||
|
import { useDisabledSongs } from './useDisabledSongs';
|
||||||
import { UI_CONSTANTS } from '../constants';
|
import { UI_CONSTANTS } from '../constants';
|
||||||
import { filterSongs } from '../utils/dataProcessing';
|
import { filterSongs } from '../utils/dataProcessing';
|
||||||
import type { Song } from '../types';
|
import type { Song } from '../types';
|
||||||
@ -13,6 +14,7 @@ export const useSearch = () => {
|
|||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const { addToQueue, toggleFavorite } = useSongOperations();
|
const { addToQueue, toggleFavorite } = useSongOperations();
|
||||||
const { showSuccess, showError } = useToast();
|
const { showSuccess, showError } = useToast();
|
||||||
|
const { disabledSongPaths, addDisabledSong, removeDisabledSong, isSongDisabled } = useDisabledSongs();
|
||||||
|
|
||||||
// Get all songs from Redux (this is memoized)
|
// Get all songs from Redux (this is memoized)
|
||||||
const allSongs = useAppSelector(selectSongsArray);
|
const allSongs = useAppSelector(selectSongsArray);
|
||||||
@ -20,11 +22,13 @@ export const useSearch = () => {
|
|||||||
// Memoize filtered results to prevent unnecessary re-computations
|
// Memoize filtered results to prevent unnecessary re-computations
|
||||||
const filteredSongs = useMemo(() => {
|
const filteredSongs = useMemo(() => {
|
||||||
if (!searchTerm.trim() || searchTerm.length < UI_CONSTANTS.SEARCH.MIN_SEARCH_LENGTH) {
|
if (!searchTerm.trim() || searchTerm.length < UI_CONSTANTS.SEARCH.MIN_SEARCH_LENGTH) {
|
||||||
return allSongs;
|
// If no search term, return all songs except disabled ones
|
||||||
|
return allSongs.filter(song => !isSongDisabled(song));
|
||||||
}
|
}
|
||||||
|
|
||||||
return filterSongs(allSongs, searchTerm);
|
// Apply both search filter and disabled songs filter
|
||||||
}, [allSongs, searchTerm]);
|
return filterSongs(allSongs, searchTerm, disabledSongPaths);
|
||||||
|
}, [allSongs, searchTerm, disabledSongPaths, isSongDisabled]);
|
||||||
|
|
||||||
// Paginate the filtered results - show all items up to current page
|
// Paginate the filtered results - show all items up to current page
|
||||||
const searchResults = useMemo(() => {
|
const searchResults = useMemo(() => {
|
||||||
@ -46,16 +50,10 @@ export const useSearch = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadMore = useCallback(() => {
|
const loadMore = useCallback(() => {
|
||||||
console.log('useSearch - loadMore called:', {
|
|
||||||
hasMore: searchResults.hasMore,
|
|
||||||
currentPage,
|
|
||||||
filteredSongsLength: filteredSongs.length,
|
|
||||||
searchResultsCount: searchResults.count
|
|
||||||
});
|
|
||||||
if (searchResults.hasMore) {
|
if (searchResults.hasMore) {
|
||||||
setCurrentPage(prev => prev + 1);
|
setCurrentPage(prev => prev + 1);
|
||||||
}
|
}
|
||||||
}, [searchResults.hasMore, currentPage, filteredSongs.length, searchResults.count]);
|
}, [searchResults.hasMore]);
|
||||||
|
|
||||||
const handleAddToQueue = useCallback(async (song: Song) => {
|
const handleAddToQueue = useCallback(async (song: Song) => {
|
||||||
try {
|
try {
|
||||||
@ -75,12 +73,26 @@ export const useSearch = () => {
|
|||||||
}
|
}
|
||||||
}, [toggleFavorite, showSuccess, showError]);
|
}, [toggleFavorite, showSuccess, showError]);
|
||||||
|
|
||||||
|
const handleToggleDisabled = useCallback(async (song: Song) => {
|
||||||
|
try {
|
||||||
|
if (isSongDisabled(song)) {
|
||||||
|
await removeDisabledSong(song);
|
||||||
|
} else {
|
||||||
|
await addDisabledSong(song);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
showError('Failed to update song disabled status');
|
||||||
|
}
|
||||||
|
}, [isSongDisabled, addDisabledSong, removeDisabledSong, showError]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
searchTerm,
|
searchTerm,
|
||||||
searchResults,
|
searchResults,
|
||||||
handleSearchChange,
|
handleSearchChange,
|
||||||
handleAddToQueue,
|
handleAddToQueue,
|
||||||
handleToggleFavorite,
|
handleToggleFavorite,
|
||||||
|
handleToggleDisabled,
|
||||||
loadMore,
|
loadMore,
|
||||||
|
isSongDisabled,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -58,6 +58,33 @@ ion-item {
|
|||||||
--color: var(--ion-text-color, #000000);
|
--color: var(--ion-text-color, #000000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Settings page specific styling */
|
||||||
|
.settings-container {
|
||||||
|
padding: 0 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-container ion-list {
|
||||||
|
margin: 0 -16px !important;
|
||||||
|
padding: 0 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-container h2 {
|
||||||
|
padding-left: 16px !important;
|
||||||
|
padding-right: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-container .bg-gray-50 {
|
||||||
|
margin: 0 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-container ion-item {
|
||||||
|
--padding-end: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-container ion-toggle {
|
||||||
|
margin-left: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Ensure accordion content is visible */
|
/* Ensure accordion content is visible */
|
||||||
ion-accordion {
|
ion-accordion {
|
||||||
--background: transparent;
|
--background: transparent;
|
||||||
|
|||||||
@ -30,11 +30,23 @@ export const selectSongsArray = createSelector(
|
|||||||
(songs) => sortSongsByArtistAndTitle(objectToArray(songs))
|
(songs) => sortSongsByArtistAndTitle(objectToArray(songs))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Selector that filters songs and excludes disabled ones
|
||||||
|
export const selectSongsArrayWithoutDisabled = createSelector(
|
||||||
|
[selectSongsArray, (_state: RootState, disabledSongPaths: Set<string>) => disabledSongPaths],
|
||||||
|
(songs, disabledSongPaths) => songs.filter(song => !disabledSongPaths.has(song.path))
|
||||||
|
);
|
||||||
|
|
||||||
export const selectFilteredSongs = createSelector(
|
export const selectFilteredSongs = createSelector(
|
||||||
[selectSongsArray, (_state: RootState, searchTerm: string) => searchTerm],
|
[selectSongsArray, (_state: RootState, searchTerm: string) => searchTerm],
|
||||||
(songs, searchTerm) => filterSongs(songs, searchTerm)
|
(songs, searchTerm) => filterSongs(songs, searchTerm)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Enhanced filtered songs that also excludes disabled songs
|
||||||
|
export const selectFilteredSongsWithoutDisabled = createSelector(
|
||||||
|
[selectSongsArray, (_state: RootState, searchTerm: string, disabledSongPaths: Set<string>) => ({ searchTerm, disabledSongPaths })],
|
||||||
|
(songs, { searchTerm, disabledSongPaths }) => filterSongs(songs, searchTerm, disabledSongPaths)
|
||||||
|
);
|
||||||
|
|
||||||
export const selectQueueArray = createSelector(
|
export const selectQueueArray = createSelector(
|
||||||
[selectQueue],
|
[selectQueue],
|
||||||
(queue) => sortQueueByOrder(objectToArray(queue))
|
(queue) => sortQueueByOrder(objectToArray(queue))
|
||||||
@ -50,16 +62,34 @@ export const selectHistoryArray = createSelector(
|
|||||||
(history) => limitArray(sortHistoryByDate(objectToArray(history)), UI_CONSTANTS.HISTORY.MAX_ITEMS)
|
(history) => limitArray(sortHistoryByDate(objectToArray(history)), UI_CONSTANTS.HISTORY.MAX_ITEMS)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// History array without disabled songs
|
||||||
|
export const selectHistoryArrayWithoutDisabled = createSelector(
|
||||||
|
[selectHistoryArray, (_state: RootState, disabledSongPaths: Set<string>) => disabledSongPaths],
|
||||||
|
(history, disabledSongPaths) => history.filter(song => !disabledSongPaths.has(song.path))
|
||||||
|
);
|
||||||
|
|
||||||
export const selectFavoritesArray = createSelector(
|
export const selectFavoritesArray = createSelector(
|
||||||
[selectFavorites],
|
[selectFavorites],
|
||||||
(favorites) => sortSongsByArtistAndTitle(objectToArray(favorites))
|
(favorites) => sortSongsByArtistAndTitle(objectToArray(favorites))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Favorites array without disabled songs
|
||||||
|
export const selectFavoritesArrayWithoutDisabled = createSelector(
|
||||||
|
[selectFavoritesArray, (_state: RootState, disabledSongPaths: Set<string>) => disabledSongPaths],
|
||||||
|
(favorites, disabledSongPaths) => favorites.filter(song => !disabledSongPaths.has(song.path))
|
||||||
|
);
|
||||||
|
|
||||||
export const selectNewSongsArray = createSelector(
|
export const selectNewSongsArray = createSelector(
|
||||||
[selectNewSongs],
|
[selectNewSongs],
|
||||||
(newSongs) => sortSongsByArtistAndTitle(objectToArray(newSongs))
|
(newSongs) => sortSongsByArtistAndTitle(objectToArray(newSongs))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// New songs array without disabled songs
|
||||||
|
export const selectNewSongsArrayWithoutDisabled = createSelector(
|
||||||
|
[selectNewSongsArray, (_state: RootState, disabledSongPaths: Set<string>) => disabledSongPaths],
|
||||||
|
(newSongs, disabledSongPaths) => newSongs.filter(song => !disabledSongPaths.has(song.path))
|
||||||
|
);
|
||||||
|
|
||||||
export const selectSingersArray = createSelector(
|
export const selectSingersArray = createSelector(
|
||||||
[selectSingers],
|
[selectSingers],
|
||||||
(singers) => objectToArray(singers).sort((a, b) => a.name.localeCompare(b.name))
|
(singers) => objectToArray(singers).sort((a, b) => a.name.localeCompare(b.name))
|
||||||
@ -109,6 +139,15 @@ export const selectSearchResults = createSelector(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Enhanced search results that exclude disabled songs
|
||||||
|
export const selectSearchResultsWithoutDisabled = createSelector(
|
||||||
|
[selectFilteredSongsWithoutDisabled],
|
||||||
|
(filteredSongs) => ({
|
||||||
|
songs: filteredSongs,
|
||||||
|
count: filteredSongs.length,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Queue-specific selectors
|
// Queue-specific selectors
|
||||||
export const selectQueueWithUserInfo = createSelector(
|
export const selectQueueWithUserInfo = createSelector(
|
||||||
[selectQueueArray, selectCurrentSinger],
|
[selectQueueArray, selectCurrentSinger],
|
||||||
|
|||||||
@ -56,6 +56,14 @@ export interface Song extends SongBase {
|
|||||||
date?: string;
|
date?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DisabledSong {
|
||||||
|
path: string;
|
||||||
|
artist: string;
|
||||||
|
title: string;
|
||||||
|
disabledAt: string;
|
||||||
|
key?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type PickedSong = {
|
export type PickedSong = {
|
||||||
song: Song
|
song: Song
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,15 +10,27 @@ export const objectToArray = <T extends { key?: string }>(
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Filter out disabled songs from an array
|
||||||
|
export const filterDisabledSongs = (songs: Song[], disabledSongPaths: Set<string>): Song[] => {
|
||||||
|
return songs.filter(song => !disabledSongPaths.has(song.path));
|
||||||
|
};
|
||||||
|
|
||||||
// Filter songs by search term with intelligent multi-word handling
|
// Filter songs by search term with intelligent multi-word handling
|
||||||
export const filterSongs = (songs: Song[], searchTerm: string): Song[] => {
|
export const filterSongs = (songs: Song[], searchTerm: string, disabledSongPaths?: Set<string>): Song[] => {
|
||||||
if (!searchTerm.trim()) return songs;
|
let filteredSongs = songs;
|
||||||
|
|
||||||
|
// First filter out disabled songs if disabledSongPaths is provided
|
||||||
|
if (disabledSongPaths) {
|
||||||
|
filteredSongs = filterDisabledSongs(songs, disabledSongPaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!searchTerm.trim()) return filteredSongs;
|
||||||
|
|
||||||
const terms = searchTerm.toLowerCase().split(/\s+/).filter(term => term.length > 0);
|
const terms = searchTerm.toLowerCase().split(/\s+/).filter(term => term.length > 0);
|
||||||
|
|
||||||
if (terms.length === 0) return songs;
|
if (terms.length === 0) return filteredSongs;
|
||||||
|
|
||||||
return songs.filter(song => {
|
return filteredSongs.filter(song => {
|
||||||
const songTitle = song.title.toLowerCase();
|
const songTitle = song.title.toLowerCase();
|
||||||
const songArtist = song.artist.toLowerCase();
|
const songArtist = song.artist.toLowerCase();
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user