Compare commits

..

2 Commits

Author SHA1 Message Date
jeffvli e6b77e5883 adjust top songs / favorite songs sections
- use table row height configuration for container calculation
- add wrapper component and use for both Top Songs and Favorite Songs
- remove the view more button, show all items by default
2026-02-13 20:07:37 -08:00
Kendall Garner d54baae3d9 improve album artist favorite performance and search 2026-02-13 09:29:00 -08:00
61 changed files with 537 additions and 1667 deletions
Binary file not shown.
-2
View File
@@ -236,8 +236,6 @@
"filter": {
"album": "$t(entity.album, {\"count\": 1})",
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
"matchAnd": "and",
"matchOr": "or",
"albumCount": "$t(entity.album, {\"count\": 2}) count",
"artist": "$t(entity.artist, {\"count\": 1})",
"biography": "biography",
+2 -3
View File
@@ -1,7 +1,6 @@
import { createSocket } from 'dgram';
import { ipcMain } from 'electron';
import { mainLogger } from '/@/main/logger';
import { DiscoveredServerItem, ServerType } from '/@/shared/types/types';
type JellyfinResponse = {
@@ -27,7 +26,7 @@ function discoverJellyfin(reply: (server: DiscoveredServerItem) => void) {
});
} catch (e) {
// Got a spurious response, ignore?
mainLogger.error('Autodiscover Jellyfin parse error', e);
console.error(e);
}
});
@@ -52,5 +51,5 @@ ipcMain.on('autodiscover-ping', (ev) => {
discoverAll((result) => port.postMessage(result))
.then(() => port.close())
.catch((err) => mainLogger.error('Autodiscover failed', err));
.catch((err) => console.error(err));
});
+3 -4
View File
@@ -7,7 +7,6 @@ import {
LyricSearchQuery,
LyricSource,
} from '.';
import { mainLogger } from '../../../logger';
import { orderSearchResults } from './shared';
const SEARCH_URL = 'https://genius.com/api/search/song';
@@ -101,7 +100,7 @@ export async function getLyricsBySongId(url: string): Promise<null | string> {
try {
result = await axios.get<string>(url, { responseType: 'text' });
} catch (e) {
mainLogger.error('Genius lyrics request failed', (e as Error)?.message);
console.error('Genius lyrics request got an error!', (e as Error)?.message);
return null;
}
@@ -139,7 +138,7 @@ export async function getSearchResults(
},
});
} catch (e) {
mainLogger.error('Genius search request failed', (e as Error)?.message);
console.error('Genius search request got an error!', (e as Error)?.message);
return null;
}
@@ -194,7 +193,7 @@ async function getSongId(
},
});
} catch (e) {
mainLogger.error('Genius search request failed', (e as Error)?.message);
console.error('Genius search request got an error!', (e as Error)?.message);
return null;
}
+2 -3
View File
@@ -1,6 +1,5 @@
import { ipcMain } from 'electron';
import { mainLogger } from '../../../logger';
import { store } from '../settings';
import { getLyricsBySongId as getGenius, getSearchResults as searchGenius } from './genius';
import { getLyricsBySongId as getLrcLib, getSearchResults as searchLrcLib } from './lrclib';
@@ -97,7 +96,7 @@ const searchAllSources = async (
allSearchResults.push(...result.value.searchResults);
} else if (result.status === 'rejected') {
const index = settled.indexOf(result);
mainLogger.error(`Error searching ${sources[index]} for lyrics`, result.reason);
console.error(`Error searching ${sources[index]} for lyrics:`, result.reason);
}
}
return allSearchResults;
@@ -161,7 +160,7 @@ const getRemoteLyrics = async (song: Song) => {
};
}
} catch (error) {
mainLogger.error(`Error fetching lyrics from ${bestMatch.source}`, error);
console.error(`Error fetching lyrics from ${bestMatch.source}:`, error);
}
if (lyricsFromSource) {
+3 -4
View File
@@ -7,7 +7,6 @@ import {
LyricSearchQuery,
LyricSource,
} from '.';
import { mainLogger } from '../../../logger';
import { orderSearchResults } from './shared';
const FETCH_URL = 'https://lrclib.net/api/get';
@@ -47,7 +46,7 @@ export async function getLyricsBySongId(songId: string): Promise<null | string>
try {
result = await axios.get<LrcLibTrackResponse>(`${FETCH_URL}/${songId}`);
} catch (e) {
mainLogger.error('LrcLib lyrics request failed', (e as Error)?.message);
console.error('LrcLib lyrics request got an error!', (e as Error)?.message);
return null;
}
@@ -70,7 +69,7 @@ export async function getSearchResults(
},
});
} catch (e) {
mainLogger.error('LrcLib search request failed', (e as Error)?.message);
console.error('LrcLib search request got an error!', (e as Error)?.message);
return null;
}
@@ -108,7 +107,7 @@ export async function query(
timeout: TIMEOUT_MS,
});
} catch (e) {
mainLogger.error('LrcLib search request failed', (e as Error).message);
console.error('LrcLib search request got an error!', (e as Error).message);
return null;
}
+2 -3
View File
@@ -6,7 +6,6 @@ import {
LyricSearchQuery,
LyricSource,
} from '.';
import { mainLogger } from '../../../logger';
import { store } from '../settings';
import { orderSearchResults } from './shared';
@@ -82,7 +81,7 @@ export async function getLyricsBySongId(songId: string): Promise<null | string>
},
});
} catch (e) {
mainLogger.error('NetEase lyrics request failed', e);
console.error('NetEase lyrics request got an error!', e);
return null;
}
const enableTranslation = store.get('enableNeteaseTranslation', false) as boolean;
@@ -115,7 +114,7 @@ export async function getSearchResults(
},
});
} catch (e) {
mainLogger.error('NetEase search request failed', e);
console.error('NetEase search request got an error!', e);
return null;
}
+4 -4
View File
@@ -1,3 +1,4 @@
import console from 'console';
import { app, ipcMain } from 'electron';
import { rm } from 'fs/promises';
import uniq from 'lodash/uniq';
@@ -6,7 +7,6 @@ import { pid } from 'node:process';
import process from 'process';
import { getMainWindow, sendToastToRenderer } from '../../../index';
import { mainLogger } from '../../../logger';
import { createLog, isWindows } from '../../../utils';
import { store } from '../settings';
@@ -109,7 +109,7 @@ const createMpv = async (data: {
try {
await mpv.start();
} catch (error: any) {
mainLogger.error('mpv failed to start', error);
console.error('mpv failed to start', error);
} finally {
await mpv.setMultipleProperties(properties || {});
}
@@ -672,7 +672,7 @@ process.on('SIGTERM', async () => {
// Handle uncaught exceptions - cleanup mpv before crashing
process.on('uncaughtException', async (error) => {
mainLogger.error('Uncaught exception', error);
console.error('Uncaught exception:', error);
await cleanupMpv(true).catch(() => {
// Ignore cleanup errors during crash
});
@@ -680,7 +680,7 @@ process.on('uncaughtException', async (error) => {
// Handle unhandled rejections - cleanup mpv
process.on('unhandledRejection', async (reason) => {
mainLogger.error('Unhandled rejection', reason);
console.error('Unhandled rejection:', reason);
await cleanupMpv(true).catch(() => {
// Ignore cleanup errors
});
+2 -3
View File
@@ -10,7 +10,6 @@ import { deflate, gzip } from 'zlib';
import manifest from './manifest.json';
import { getMainWindow } from '/@/main/index';
import { mainLogger } from '/@/main/logger';
import { isLinux } from '/@/main/utils';
import { QueueSong } from '/@/shared/types/domain-types';
import { ClientEvent, ServerEvent } from '/@/shared/types/remote-types';
@@ -350,7 +349,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
}, 10000) as unknown as number;
}
ws.on('error', (err) => mainLogger.error('Remote WebSocket error', err));
ws.on('error', console.error);
ws.on('message', (data) => {
try {
@@ -489,7 +488,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
}
}
} catch (error) {
mainLogger.error('Remote message handler error', error);
console.error(error);
}
});
+14 -15
View File
@@ -29,7 +29,6 @@ import packageJson from '../../package.json';
import { disableMediaKeys, enableMediaKeys } from './features/core/player/media-keys';
import { shutdownServer } from './features/core/remote';
import { store } from './features/core/settings';
import { mainLogger } from './logger';
import MenuBuilder from './menu';
import {
autoUpdaterLogInterface,
@@ -67,7 +66,7 @@ type UpdaterInstance = AppImageUpdater | MacUpdater | NsisUpdater | typeof autoU
class AppUpdater {
constructor() {
const effectiveChannel = store.get('release_channel') as string;
mainLogger.info('Effective update channel:', effectiveChannel);
console.log('Effective update channel:', effectiveChannel);
if (effectiveChannel === 'alpha') {
checkAllChannelsAndGetBest().then(({ updater: updaterInstance }) => {
updaterInstance.autoInstallOnAppQuit = true;
@@ -104,7 +103,7 @@ async function checkAllChannelsAndGetBest(): Promise<{
alphaUpdater.allowDowngrade = true;
try {
mainLogger.info('Checking for updates on alpha channel');
console.log('Checking for updates on alpha channel');
const alphaResult = await alphaUpdater.checkForUpdates();
if (
alphaResult?.updateInfo?.version &&
@@ -121,7 +120,7 @@ async function checkAllChannelsAndGetBest(): Promise<{
try {
autoUpdater.setFeedURL(GITHUB_UPDATER_CONFIG);
configureAutoUpdaterForChannel('latest');
mainLogger.info('Checking for updates on latest channel (GitHub)');
console.log('Checking for updates on latest channel (GitHub)');
const latestResult = await autoUpdater.checkForUpdates();
if (
latestResult?.updateInfo?.version &&
@@ -156,13 +155,13 @@ function configureAndGetUpdater(): UpdaterInstance {
let releaseChannel = store.get('release_channel');
const isNotConfigured = !releaseChannel;
mainLogger.info('Release channel:', releaseChannel);
mainLogger.info('Is beta version:', isBetaVersion);
mainLogger.info('Is alpha version:', isAlphaVersion);
mainLogger.info('Is not configured:', isNotConfigured);
console.log('Release channel:', releaseChannel);
console.log('Is beta version:', isBetaVersion);
console.log('Is alpha version:', isAlphaVersion);
console.log('Is not configured:', isNotConfigured);
if (isNotConfigured) {
mainLogger.info('Release channel not configured, setting default channel');
console.log('Release channel not configured, setting default channel');
const defaultChannel = isAlphaVersion ? 'alpha' : isBetaVersion ? 'beta' : 'latest';
store.set('release_channel', defaultChannel);
releaseChannel = defaultChannel;
@@ -236,7 +235,7 @@ function createAlphaUpdaterInstance(): AppImageUpdater | MacUpdater | NsisUpdate
protocol.registerSchemesAsPrivileged([{ privileges: { bypassCSP: true }, scheme: 'feishin' }]);
process.on('uncaughtException', (error: any) => {
mainLogger.error('Uncaught exception in main process', error);
console.error('Error in main process', error);
});
if (store.get('ignore_ssl')) {
@@ -522,12 +521,12 @@ async function createWindow(first = true): Promise<void> {
'app-check-for-updates',
async (): Promise<{ updateAvailable: boolean; version?: string }> => {
if (disableAutoUpdates()) {
mainLogger.info('Auto updates are disabled');
console.log('Auto updates are disabled');
return { updateAvailable: false };
}
try {
mainLogger.info('Checking for updates');
console.log('Checking for updates');
const effectiveChannel = store.get('release_channel') as string;
let result: null | UpdateCheckResult;
let updater: UpdaterInstance;
@@ -542,9 +541,9 @@ async function createWindow(first = true): Promise<void> {
}
const updateAvailable = result?.isUpdateAvailable ?? false;
mainLogger.info('Update available:', updateAvailable);
console.log('Update available:', updateAvailable);
if (updateAvailable && store.get('disable_auto_updates') !== true) {
mainLogger.info('Downloading update');
console.log('Downloading update');
updater.downloadUpdate();
}
@@ -553,7 +552,7 @@ async function createWindow(first = true): Promise<void> {
version: result?.updateInfo?.version,
};
} catch {
mainLogger.error('Error checking for updates');
console.log('Error checking for updates');
return { updateAvailable: false };
}
},
-36
View File
@@ -1,36 +0,0 @@
const pad = (n: number) => String(n).padStart(2, '0');
const timestamp = () => {
const d = new Date();
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
};
const format = (level: string, message: string, ...args: unknown[]) => {
const prefix = `[${timestamp()}] [${level}] ${message}`;
if (args.length > 0) {
console.log(prefix, ...args);
} else {
console.log(prefix);
}
};
export const mainLogger = {
debug: (message: string, ...args: unknown[]) => format('DEBUG', message, ...args),
error: (message: string, ...args: unknown[]) => {
const prefix = `[${timestamp()}] [ERROR] ${message}`;
if (args.length > 0) {
console.error(prefix, ...args);
} else {
console.error(prefix);
}
},
info: (message: string, ...args: unknown[]) => format('INFO', message, ...args),
warn: (message: string, ...args: unknown[]) => {
const prefix = `[${timestamp()}] [WARN] ${message}`;
if (args.length > 0) {
console.warn(prefix, ...args);
} else {
console.warn(prefix);
}
},
};
+87 -59
View File
@@ -4,6 +4,7 @@ import { immer } from 'zustand/middleware/immer';
import { createWithEqualityFn } from 'zustand/traditional';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { logMsg } from '/@/renderer/utils/logger-message';
import { toast } from '/@/shared/components/toast/toast';
import { ClientEvent, ServerEvent, SongUpdateSocket } from '/@/shared/types/remote-types';
@@ -41,7 +42,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
immer((set, get) => ({
actions: {
reconnect: async () => {
logFn.debug('Reconnect initiated', {
logFn.debug(logMsg[LogCategory.REMOTE].reconnectInitiated, {
category: LogCategory.REMOTE,
});
const existing = get().socket;
@@ -51,7 +52,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
existing.readyState === WebSocket.OPEN ||
existing.readyState === WebSocket.CONNECTING
) {
logFn.debug('Closing existing socket', {
logFn.debug(logMsg[LogCategory.REMOTE].closingExistingSocket, {
category: LogCategory.REMOTE,
meta: { readyState: existing.readyState },
});
@@ -63,17 +64,17 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
let authHeader: string | undefined;
try {
logFn.debug('Fetching credentials', {
logFn.debug(logMsg[LogCategory.REMOTE].fetchingCredentials, {
category: LogCategory.REMOTE,
});
const credentials = await fetch('/credentials');
authHeader = await credentials.text();
logFn.debug('Credentials fetched', {
logFn.debug(logMsg[LogCategory.REMOTE].credentialsFetched, {
category: LogCategory.REMOTE,
meta: { hasAuthHeader: !!authHeader },
});
} catch (error) {
logFn.error('Failed to get credentials', {
logFn.error(logMsg[LogCategory.REMOTE].failedToGetCredentials, {
category: LogCategory.REMOTE,
meta: { error },
});
@@ -81,7 +82,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
set((state) => {
const wsUrl = location.href.replace('http', 'ws');
logFn.debug('Creating new WebSocket', {
logFn.debug(logMsg[LogCategory.REMOTE].creatingWebSocket, {
category: LogCategory.REMOTE,
meta: { url: wsUrl },
});
@@ -92,28 +93,34 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
socket.addEventListener('message', (message) => {
const { data, event } = JSON.parse(message.data) as ServerEvent;
logFn.debug('WebSocket message received', {
logFn.debug(logMsg[LogCategory.REMOTE].webSocketMessageReceived, {
category: LogCategory.REMOTE,
meta: { data, event },
});
switch (event) {
case 'error': {
logFn.error('WebSocket error event', {
category: LogCategory.REMOTE,
meta: { data },
});
logFn.error(
logMsg[LogCategory.REMOTE].webSocketErrorEvent,
{
category: LogCategory.REMOTE,
meta: { data },
},
);
toast.error({ message: data, title: 'Socket error' });
break;
}
case 'favorite': {
logFn.debug('Favorite event received', {
category: LogCategory.REMOTE,
meta: {
favorite: data.favorite,
id: data.id,
logFn.debug(
logMsg[LogCategory.REMOTE].favoriteEventReceived,
{
category: LogCategory.REMOTE,
meta: {
favorite: data.favorite,
id: data.id,
},
},
});
);
set((state) => {
if (state.info.song?.id === data.id) {
state.info.song.userFavorite = data.favorite;
@@ -122,27 +129,33 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
break;
}
case 'playback': {
logFn.debug('Playback event received', {
category: LogCategory.REMOTE,
meta: { status: data },
});
logFn.debug(
logMsg[LogCategory.REMOTE].playbackEventReceived,
{
category: LogCategory.REMOTE,
meta: { status: data },
},
);
set((state) => {
state.info.status = data;
});
break;
}
case 'position': {
logFn.debug('Position event received', {
category: LogCategory.REMOTE,
meta: { position: data },
});
logFn.debug(
logMsg[LogCategory.REMOTE].positionEventReceived,
{
category: LogCategory.REMOTE,
meta: { position: data },
},
);
set((state) => {
state.info.position = data;
});
break;
}
case 'proxy': {
logFn.debug('Proxy event received (image update)', {
logFn.debug(logMsg[LogCategory.REMOTE].proxyEventReceived, {
category: LogCategory.REMOTE,
meta: {
dataLength: data?.length,
@@ -157,13 +170,16 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
break;
}
case 'rating': {
logFn.debug('Rating event received', {
category: LogCategory.REMOTE,
meta: {
id: data.id,
rating: data.rating,
logFn.debug(
logMsg[LogCategory.REMOTE].ratingEventReceived,
{
category: LogCategory.REMOTE,
meta: {
id: data.id,
rating: data.rating,
},
},
});
);
set((state) => {
if (state.info.song?.id === data.id) {
state.info.song.userRating = data.rating;
@@ -172,27 +188,33 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
break;
}
case 'repeat': {
logFn.debug('Repeat event received', {
category: LogCategory.REMOTE,
meta: { repeat: data },
});
logFn.debug(
logMsg[LogCategory.REMOTE].repeatEventReceived,
{
category: LogCategory.REMOTE,
meta: { repeat: data },
},
);
set((state) => {
state.info.repeat = data;
});
break;
}
case 'shuffle': {
logFn.debug('Shuffle event received', {
category: LogCategory.REMOTE,
meta: { shuffle: data },
});
logFn.debug(
logMsg[LogCategory.REMOTE].shuffleEventReceived,
{
category: LogCategory.REMOTE,
meta: { shuffle: data },
},
);
set((state) => {
state.info.shuffle = data;
});
break;
}
case 'song': {
logFn.debug('Song event received', {
logFn.debug(logMsg[LogCategory.REMOTE].songEventReceived, {
category: LogCategory.REMOTE,
meta: {
artistName: data?.artistName,
@@ -206,7 +228,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
break;
}
case 'state': {
logFn.debug('State event received (full state update)', {
logFn.debug(logMsg[LogCategory.REMOTE].stateEventReceived, {
category: LogCategory.REMOTE,
meta: {
hasSong: !!data.song,
@@ -221,10 +243,13 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
break;
}
case 'volume': {
logFn.debug('Volume event received', {
category: LogCategory.REMOTE,
meta: { volume: data },
});
logFn.debug(
logMsg[LogCategory.REMOTE].volumeEventReceived,
{
category: LogCategory.REMOTE,
meta: { volume: data },
},
);
set((state) => {
state.info.volume = data;
});
@@ -233,7 +258,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
});
socket.addEventListener('open', () => {
logFn.debug('WebSocket opened', {
logFn.debug(logMsg[LogCategory.REMOTE].webSocketOpened, {
category: LogCategory.REMOTE,
meta: {
hasAuthHeader: !!authHeader,
@@ -241,7 +266,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
},
});
if (authHeader) {
logFn.debug('Sending authentication', {
logFn.debug(logMsg[LogCategory.REMOTE].sendingAuthentication, {
category: LogCategory.REMOTE,
});
socket.send(
@@ -255,7 +280,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
});
socket.addEventListener('close', (reason) => {
logFn.debug('WebSocket closed', {
logFn.debug(logMsg[LogCategory.REMOTE].webSocketClosed, {
category: LogCategory.REMOTE,
meta: {
code: reason.code,
@@ -265,13 +290,13 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
},
});
if (reason.code === 4002 || reason.code === 4003) {
logFn.debug('Reloading page due to close code', {
logFn.debug(logMsg[LogCategory.REMOTE].reloadingPage, {
category: LogCategory.REMOTE,
meta: { code: reason.code },
});
location.reload();
} else if (reason.code === 4000) {
logFn.warn('Server is down', {
logFn.warn(logMsg[LogCategory.REMOTE].serverIsDown, {
category: LogCategory.REMOTE,
});
toast.warn({
@@ -279,13 +304,16 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
title: 'Connection closed',
});
} else if (reason.code !== 4001 && !socket.natural) {
logFn.error('Socket closed unexpectedly', {
category: LogCategory.REMOTE,
meta: {
code: reason.code,
reason: reason.reason,
logFn.error(
logMsg[LogCategory.REMOTE].socketClosedUnexpectedly,
{
category: LogCategory.REMOTE,
meta: {
code: reason.code,
reason: reason.reason,
},
},
});
);
toast.error({
message: 'Socket closed for unexpected reason',
title: 'Connection closed',
@@ -303,7 +331,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
send: (data: ClientEvent) => {
const socket = get().socket;
if (socket) {
logFn.debug('Sending event to server', {
logFn.debug(logMsg[LogCategory.REMOTE].sendingEventToServer, {
category: LogCategory.REMOTE,
meta: {
data: data,
@@ -313,7 +341,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
});
socket.send(JSON.stringify(data));
} else {
logFn.warn('Cannot send event - socket not available', {
logFn.warn(logMsg[LogCategory.REMOTE].cannotSendEvent, {
category: LogCategory.REMOTE,
meta: { event: data.event },
});
-10
View File
@@ -4,7 +4,6 @@ import { NavidromeController } from '/@/renderer/api/navidrome/navidrome-control
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
import { mergeMusicFolderId } from '/@/renderer/api/utils-music-folder';
import { getServerById, useAuthStore, useSettingsStore } from '/@/renderer/store';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { toast } from '/@/shared/components/toast/toast';
import {
AuthenticationResponse,
@@ -32,7 +31,6 @@ const apiController = <K extends keyof ControllerEndpoint>(
const serverType = type || useAuthStore.getState().currentServer?.type;
if (!serverType) {
logFn.warn('No server selected', { category: LogCategory.API });
toast.error({
message: i18n.t('error.serverNotSelectedError', {
postProcess: 'sentenceCase',
@@ -45,10 +43,6 @@ const apiController = <K extends keyof ControllerEndpoint>(
const controllerFn = endpoints?.[serverType]?.[endpoint];
if (typeof controllerFn !== 'function') {
logFn.warn('Endpoint not implemented', {
category: LogCategory.API,
meta: { endpoint, serverType },
});
toast.error({
message: `Endpoint ${endpoint} is not implemented for ${serverType}`,
title: i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' }) as string,
@@ -63,10 +57,6 @@ const apiController = <K extends keyof ControllerEndpoint>(
);
}
logFn.debug('API controller call', {
category: LogCategory.API,
meta: { endpoint, serverType },
});
return controllerFn;
};
+3 -17
View File
@@ -8,7 +8,6 @@ import qs from 'qs';
import i18n from '/@/i18n/i18n';
import { authenticationFailure } from '/@/renderer/api/utils';
import { useAuthStore } from '/@/renderer/store';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { getServerUrl } from '/@/renderer/utils/normalize-server-url';
import { ndType } from '/@/shared/api/navidrome/navidrome-types';
import { resultWithHeaders } from '/@/shared/api/utils';
@@ -368,21 +367,11 @@ axiosClient.interceptors.response.use(
})
.catch((newError: any) => {
if (newError !== TIMEOUT_ERROR) {
logFn.error('Reauthentication failed', {
category: LogCategory.API,
meta: {
message: (newError as Error)?.message,
serverId: currentServer.id,
},
});
console.error('Error when trying to reauthenticate: ', newError);
if (isAxiosError(newError) && newError.code === 'ERR_NETWORK') {
logFn.info(
console.log(
'Network error during reauthentication - preserving credentials',
{
category: LogCategory.API,
meta: { serverId: currentServer.id },
},
);
} else {
limitedFail(currentServer);
@@ -398,10 +387,7 @@ axiosClient.interceptors.response.use(
}
if (isAxiosError(error) && error.code === 'ERR_NETWORK') {
logFn.info('Network error during authentication - preserving credentials', {
category: LogCategory.API,
meta: { serverId: useAuthStore.getState().currentServer?.id },
});
console.log('Network error during authentication - preserving credentials');
} else {
limitedFail(currentServer);
}
+2 -5
View File
@@ -1,19 +1,16 @@
import { useAuthStore } from '/@/renderer/store';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { toast } from '/@/shared/components/toast/toast';
import { ServerListItem } from '/@/shared/types/types';
export const authenticationFailure = (currentServer: null | ServerListItem) => {
logFn.error('Token expired', {
category: LogCategory.API,
meta: { serverId: currentServer?.id },
});
toast.error({
message: 'Your session has expired.',
});
if (currentServer) {
const serverId = currentServer.id;
const token = currentServer.ndCredential;
console.error(`token is expired: ${token}`);
useAuthStore.getState().actions.updateServer(serverId, { ndCredential: undefined });
useAuthStore.getState().actions.setCurrentServer(null);
}
@@ -17,6 +17,7 @@ import {
useSettingsStore,
} from '/@/renderer/store';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { logMsg } from '/@/renderer/utils/logger-message';
import { LyricSource, ServerType } from '/@/shared/types/domain-types';
import { FontType, Platform, PlayerStyle, PlayerType } from '/@/shared/types/types';
@@ -269,7 +270,7 @@ export const useAppTracker = () => {
if (lastTrackedDate !== todayUTC) {
appTrackerInFlight = true;
const properties = getProperties();
logFn.info('Analytics sent', {
logFn.info(logMsg[LogCategory.ANALYTICS].appTracked, {
category: LogCategory.ANALYTICS,
meta: { properties, todayUTC },
});
@@ -289,7 +290,7 @@ export const useAppTracker = () => {
appTrackerLastSentDate = utcDate;
localStorage.setItem('analytics_app_tracker_timestamp', utcDate);
logFn.debug('Analytics sent', {
logFn.debug(logMsg[LogCategory.ANALYTICS].appTracked, {
category: LogCategory.ANALYTICS,
meta: { properties },
});
@@ -5,6 +5,7 @@ import { useLocation } from 'react-router';
import { isAnalyticsDisabled } from '/@/renderer/features/analytics/hooks/use-analytics-disabled';
import { getRoutePattern } from '/@/renderer/features/analytics/utils/get-route-pattern';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { logMsg } from '/@/renderer/utils/logger-message';
const trackPageView = (routePattern: string) => {
window.umami?.track((props) => ({
@@ -27,7 +28,7 @@ export const usePageTracker = () => {
trackPageViewMutation(routePattern, {
onSettled: () => {
logFn.debug('Page view tracked', {
logFn.debug(logMsg[LogCategory.ANALYTICS].pageViewTracked, {
category: LogCategory.ANALYTICS,
meta: { route: routePattern },
});
@@ -225,6 +225,39 @@ const AlbumArtistMetadataBiography = ({
);
};
const TABLE_ROW_HEIGHT = {
compact: 40,
default: 64,
large: 88,
} as const;
const TABLE_HEADER_HEIGHT = 40;
interface SongTableListContainerProps {
children: React.ReactNode;
enableHeader?: boolean;
itemCount: number;
maxRows?: number;
tableSize?: 'compact' | 'default' | 'large';
}
function getTableRowHeight(size: 'compact' | 'default' | 'large' | undefined): number {
return size ? TABLE_ROW_HEIGHT[size] : TABLE_ROW_HEIGHT.default;
}
const SongTableListContainer = ({
children,
enableHeader = true,
itemCount,
maxRows = 5,
tableSize = 'default',
}: SongTableListContainerProps) => {
const rowHeight = getTableRowHeight(tableSize);
const headerOffset = enableHeader ? TABLE_HEADER_HEIGHT : 0;
const height = headerOffset + rowHeight * Math.min(itemCount, maxRows);
return <div style={{ height }}>{children}</div>;
};
interface AlbumArtistMetadataTopSongsProps {
detailQuery: ReturnType<typeof useSuspenseQuery<AlbumArtistDetailResponse>>;
routeId: string;
@@ -237,7 +270,6 @@ const AlbumArtistMetadataTopSongsContent = ({
const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState('');
const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 300);
const [showAll, setShowAll] = useState(false);
const [topSongsQueryType, setTopSongsQueryType] = useLocalStorage<'community' | 'personal'>({
defaultValue: 'community',
key: 'album-artist-top-songs-query-type',
@@ -269,13 +301,8 @@ const AlbumArtistMetadataTopSongsContent = ({
}, [tableConfig?.columns]);
const filteredSongs = useMemo(() => {
const filtered = searchLibraryItems(songs, debouncedSearchTerm, LibraryItem.SONG);
// When searching, show all results. Otherwise, limit to 5 if not showing all
if (debouncedSearchTerm?.trim() || showAll) {
return filtered;
}
return filtered.slice(0, 5);
}, [songs, debouncedSearchTerm, showAll]);
return searchLibraryItems(songs, debouncedSearchTerm, LibraryItem.SONG);
}, [songs, debouncedSearchTerm]);
const { handleColumnReordered } = useItemListColumnReorder({
itemListKey: ItemListKey.SONG,
@@ -459,35 +486,35 @@ const AlbumArtistMetadataTopSongsContent = ({
tableColumnsData={SONG_TABLE_COLUMNS}
/>
</Group>
<ItemTableList
activeRowId={currentSongId}
autoFitColumns={tableConfig.autoFitColumns}
CellComponent={ItemTableListColumn}
columns={columns}
data={filteredSongs}
enableAlternateRowColors={tableConfig.enableAlternateRowColors}
enableDrag
enableDragScroll={false}
enableExpansion={false}
<SongTableListContainer
enableHeader={tableConfig.enableHeader}
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
enableSelection
enableSelectionDialog={false}
enableVerticalBorders={tableConfig.enableVerticalBorders}
itemType={LibraryItem.SONG}
onColumnReordered={handleColumnReordered}
onColumnResized={handleColumnResized}
overrideControls={overrideControls}
size={tableConfig.size}
/>
{!searchTerm.trim() && songs.length > 5 && !showAll && (
<Group justify="center" w="100%">
<Button onClick={() => setShowAll(true)} variant="subtle">
{t('action.viewMore', { postProcess: 'sentenceCase' })}
</Button>
</Group>
)}
itemCount={filteredSongs.length}
maxRows={5}
tableSize={tableConfig.size}
>
<ItemTableList
activeRowId={currentSongId}
autoFitColumns={tableConfig.autoFitColumns}
CellComponent={ItemTableListColumn}
columns={columns}
data={filteredSongs}
enableAlternateRowColors={tableConfig.enableAlternateRowColors}
enableDrag
enableDragScroll={false}
enableExpansion={false}
enableHeader={tableConfig.enableHeader}
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
enableSelection
enableSelectionDialog={false}
enableVerticalBorders={tableConfig.enableVerticalBorders}
itemType={LibraryItem.SONG}
onColumnReordered={handleColumnReordered}
onColumnResized={handleColumnResized}
overrideControls={overrideControls}
size={tableConfig.size}
/>
</SongTableListContainer>
</>
) : null}
</Stack>
@@ -523,7 +550,6 @@ const AlbumArtistMetadataFavoriteSongs = ({ routeId }: AlbumArtistMetadataFavori
const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState('');
const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 300);
const [showAll, setShowAll] = useState(false);
const tableConfig = useSettingsStore((state) => state.lists[ItemListKey.SONG]?.table);
const currentSong = usePlayerSong();
const player = usePlayer();
@@ -548,13 +574,8 @@ const AlbumArtistMetadataFavoriteSongs = ({ routeId }: AlbumArtistMetadataFavori
}, [tableConfig?.columns]);
const filteredSongs = useMemo(() => {
const filtered = searchLibraryItems(songs, debouncedSearchTerm, LibraryItem.SONG);
// When searching, show all results. Otherwise, limit to 5 if not showing all
if (debouncedSearchTerm?.trim() || showAll) {
return filtered;
}
return filtered.slice(0, 5);
}, [songs, debouncedSearchTerm, showAll]);
return searchLibraryItems(songs, debouncedSearchTerm, LibraryItem.SONG);
}, [songs, debouncedSearchTerm]);
const { handleColumnReordered } = useItemListColumnReorder({
itemListKey: ItemListKey.SONG,
@@ -717,35 +738,35 @@ const AlbumArtistMetadataFavoriteSongs = ({ routeId }: AlbumArtistMetadataFavori
tableColumnsData={SONG_TABLE_COLUMNS}
/>
</Group>
<ItemTableList
activeRowId={currentSongId}
autoFitColumns={tableConfig.autoFitColumns}
CellComponent={ItemTableListColumn}
columns={columns}
data={filteredSongs}
enableAlternateRowColors={tableConfig.enableAlternateRowColors}
enableDrag
enableDragScroll={false}
enableExpansion={false}
<SongTableListContainer
enableHeader={tableConfig.enableHeader}
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
enableSelection
enableSelectionDialog={false}
enableVerticalBorders={tableConfig.enableVerticalBorders}
itemType={LibraryItem.SONG}
onColumnReordered={handleColumnReordered}
onColumnResized={handleColumnResized}
overrideControls={overrideControls}
size={tableConfig.size}
/>
{!searchTerm.trim() && songs.length > 5 && !showAll && (
<Group justify="center" w="100%">
<Button onClick={() => setShowAll(true)} variant="subtle">
{t('action.viewMore', { postProcess: 'sentenceCase' })}
</Button>
</Group>
)}
itemCount={filteredSongs.length}
maxRows={5}
tableSize={tableConfig.size}
>
<ItemTableList
activeRowId={currentSongId}
autoFitColumns={tableConfig.autoFitColumns}
CellComponent={ItemTableListColumn}
columns={columns}
data={filteredSongs}
enableAlternateRowColors={tableConfig.enableAlternateRowColors}
enableDrag
enableDragScroll={false}
enableExpansion={false}
enableHeader={tableConfig.enableHeader}
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
enableSelection
enableSelectionDialog={false}
enableVerticalBorders={tableConfig.enableVerticalBorders}
itemType={LibraryItem.SONG}
onColumnReordered={handleColumnReordered}
onColumnResized={handleColumnResized}
overrideControls={overrideControls}
size={tableConfig.size}
/>
</SongTableListContainer>
</>
) : null}
</Stack>
@@ -21,6 +21,7 @@ import {
} from '/@/renderer/store';
import { sentenceCase } from '/@/renderer/utils';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { logMsg } from '/@/renderer/utils/logger-message';
import { useDebouncedCallback } from '/@/shared/hooks/use-debounced-callback';
import { LibraryItem, QueueSong, ServerType } from '/@/shared/types/domain-types';
import { PlayerStatus } from '/@/shared/types/types';
@@ -89,7 +90,7 @@ export const useDiscordRpc = () => {
reason = 'paused_with_show_paused_disabled';
}
logFn.debug('Activity was cleared for Discord RPC', {
logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcActivityCleared, {
category: LogCategory.EXTERNAL,
meta: {
reason,
@@ -127,7 +128,7 @@ export const useDiscordRpc = () => {
const isConnected = await discordRpc?.isConnected();
if (!isConnected) {
logFn.debug('Discord RPC was initialized', {
logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcInitialized, {
category: LogCategory.EXTERNAL,
meta: { clientId: discordSettings.clientId },
});
@@ -135,7 +136,7 @@ export const useDiscordRpc = () => {
await discordRpc?.initialize(discordSettings.clientId);
}
logFn.debug('Activity was set for Discord RPC', {
logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcSetActivity, {
category: LogCategory.EXTERNAL,
meta: {
currentStatus: current[2],
@@ -167,7 +168,7 @@ export const useDiscordRpc = () => {
current[2] !== previous[2]
) {
if (trackChangedByState || trackChanged) {
logFn.debug('Track was changed for Discord RPC', {
logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcTrackChanged, {
category: LogCategory.EXTERNAL,
meta: {
artistName: song.artists?.[0]?.name,
@@ -314,7 +315,7 @@ export const useDiscordRpc = () => {
// Initialize if needed
const isConnected = await discordRpc?.isConnected();
if (!isConnected) {
logFn.debug('Discord RPC was initialized', {
logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcInitialized, {
category: LogCategory.EXTERNAL,
meta: {
clientId: discordSettings.clientId,
@@ -326,7 +327,7 @@ export const useDiscordRpc = () => {
await discordRpc?.initialize(discordSettings.clientId);
}
logFn.debug('Activity was set for Discord RPC', {
logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcSetActivity, {
category: LogCategory.EXTERNAL,
meta: {
albumName: song.album,
@@ -346,7 +347,7 @@ export const useDiscordRpc = () => {
});
discordRpc?.setActivity(activity);
} else {
logFn.debug('Activity was not updated for Discord RPC', {
logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcUpdateSkipped, {
category: LogCategory.EXTERNAL,
meta: {
currentStatus: current[2],
@@ -383,7 +384,7 @@ export const useDiscordRpc = () => {
// Quit Discord RPC if it was enabled and is now disabled
useEffect(() => {
if ((!discordSettings.enabled || privateMode) && Boolean(previousEnabledRef.current)) {
logFn.info('Discord RPC was quit', {
logFn.info(logMsg[LogCategory.EXTERNAL].discordRpcQuit, {
category: LogCategory.EXTERNAL,
meta: {
enabled: discordSettings.enabled,
@@ -18,7 +18,6 @@ import { AnimatedPage } from '/@/renderer/features/shared/components/animated-pa
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
import { AppRoute } from '/@/renderer/router/routes';
import { useAuthStoreActions, useCurrentServer } from '/@/renderer/store';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { Button } from '/@/shared/components/button/button';
import { Center } from '/@/shared/components/center/center';
import { Code } from '/@/shared/components/code/code';
@@ -137,10 +136,6 @@ const LoginRoute = () => {
);
if (!data) {
logFn.error('Login failed (no data returned)', {
category: LogCategory.SYSTEM,
meta: { serverName, serverType, serverUrl },
});
return toast.error({
message: t('error.authenticationFailed', { postProcess: 'sentenceCase' }),
});
@@ -164,10 +159,6 @@ const LoginRoute = () => {
addServer(serverItem);
setCurrentServer(serverItem);
logFn.info('Login successful', {
category: LogCategory.SYSTEM,
meta: { serverName, serverType, serverUrl, userId: data.userId },
});
toast.success({
message: t('form.addServer.success', { postProcess: 'sentenceCase' }),
});
@@ -184,10 +175,6 @@ const LoginRoute = () => {
}
}
} catch (err: any) {
logFn.error('Login failed', {
category: LogCategory.SYSTEM,
meta: { message: err?.message, serverName, serverType, serverUrl },
});
setIsLoading(false);
return toast.error({ message: err?.message });
}
@@ -6,6 +6,7 @@ import { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'r
import { AudioPlayer, PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types';
import { convertToLogVolume } from '/@/renderer/features/player/audio-player/utils/player-utils';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { logMsg } from '/@/renderer/utils/logger-message';
import { PlayerStatus } from '/@/shared/types/types';
export interface WebPlayerEngineHandle extends AudioPlayer {
@@ -159,7 +160,7 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
const { error } = target;
logFn.error('An error occurred during playback', {
logFn.error(logMsg[LogCategory.PLAYER].playbackError, {
category: LogCategory.PLAYER,
meta: { error },
});
@@ -19,6 +19,7 @@ import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-a
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
import { AddToQueueType, usePlayerActions, useSettingsStore } from '/@/renderer/store';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { logMsg } from '/@/renderer/utils/logger-message';
import { shuffle as shuffleArray } from '/@/renderer/utils/shuffle';
import { sortSongsByFetchedOrder } from '/@/shared/api/utils';
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
@@ -201,7 +202,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
if (typeof type === 'object' && 'edge' in type && type.edge !== null) {
const edge = type.edge === 'top' ? 'top' : 'bottom';
logFn.debug('Added to queue by data', {
logFn.debug(logMsg[LogCategory.PLAYER].addToQueueByData, {
category: LogCategory.PLAYER,
meta: {
data: data.length,
@@ -214,7 +215,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
storeActions.addToQueueByUniqueId(filteredData, type.uniqueId, edge, playSongId);
} else {
logFn.debug('Added to queue by type', {
logFn.debug(logMsg[LogCategory.PLAYER].addToQueueByType, {
category: LogCategory.PLAYER,
meta: { data: data.length, filtered: filteredData.length, type },
});
@@ -257,7 +258,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
};
try {
logFn.debug('Added to queue by fetch', {
logFn.debug(logMsg[LogCategory.PLAYER].addToQueueByFetch, {
category: LogCategory.PLAYER,
meta: { ids: id, itemType, serverId, type },
});
@@ -323,7 +324,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
let toastId: null | string = null;
let fetchId: null | string = null;
logFn.debug('Added to queue by list query', {
logFn.debug(logMsg[LogCategory.PLAYER].addToQueueByListQuery, {
category: LogCategory.PLAYER,
meta: { itemType, query, serverId, type },
});
@@ -404,7 +405,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
postProcess: 'sentenceCase',
}),
onClose: () => {
logFn.debug('Cancelled fetch', {
logFn.debug(logMsg[LogCategory.PLAYER].cancelledFetch, {
category: LogCategory.PLAYER,
meta: { itemType, serverId },
});
@@ -504,7 +505,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
);
const clearQueue = useCallback(() => {
logFn.debug('Cleared queue', {
logFn.debug(logMsg[LogCategory.PLAYER].clearQueue, {
category: LogCategory.PLAYER,
});
@@ -513,7 +514,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
const clearSelected = useCallback(
(items: QueueSong[]) => {
logFn.debug('Cleared selected', {
logFn.debug(logMsg[LogCategory.PLAYER].clearSelected, {
category: LogCategory.PLAYER,
meta: { items: items.length },
});
@@ -525,7 +526,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
const decreaseVolume = useCallback(
(amount: number) => {
logFn.debug('Decreased volume', {
logFn.debug(logMsg[LogCategory.PLAYER].decreaseVolume, {
category: LogCategory.PLAYER,
meta: { amount },
});
@@ -537,7 +538,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
const increaseVolume = useCallback(
(amount: number) => {
logFn.debug('Increased volume', {
logFn.debug(logMsg[LogCategory.PLAYER].increaseVolume, {
category: LogCategory.PLAYER,
meta: { amount },
});
@@ -548,7 +549,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
);
const mediaNext = useCallback(() => {
logFn.debug('Media next', {
logFn.debug(logMsg[LogCategory.PLAYER].mediaNext, {
category: LogCategory.PLAYER,
});
@@ -556,7 +557,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
}, [storeActions]);
const mediaPause = useCallback(() => {
logFn.debug('Media pause', {
logFn.debug(logMsg[LogCategory.PLAYER].mediaPause, {
category: LogCategory.PLAYER,
});
@@ -565,7 +566,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
const mediaPlay = useCallback(
(id?: string) => {
logFn.debug('Media play', {
logFn.debug(logMsg[LogCategory.PLAYER].mediaPlay, {
category: LogCategory.PLAYER,
meta: { id },
});
@@ -577,7 +578,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
const mediaPlayByIndex = useCallback(
(index: number) => {
logFn.debug('Media play by index', {
logFn.debug(logMsg[LogCategory.PLAYER].mediaPlayByIndex, {
category: LogCategory.PLAYER,
meta: { index },
});
@@ -588,7 +589,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
);
const mediaPrevious = useCallback(() => {
logFn.debug('Media previous', {
logFn.debug(logMsg[LogCategory.PLAYER].mediaPrevious, {
category: LogCategory.PLAYER,
});
@@ -596,7 +597,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
}, [storeActions]);
const mediaStop = useCallback(() => {
logFn.debug('Media stop', {
logFn.debug(logMsg[LogCategory.PLAYER].mediaStop, {
category: LogCategory.PLAYER,
});
@@ -605,7 +606,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
const mediaSeekToTimestamp = useCallback(
(timestamp: number) => {
logFn.debug('Media seek to timestamp', {
logFn.debug(logMsg[LogCategory.PLAYER].mediaSeekToTimestamp, {
category: LogCategory.PLAYER,
meta: { timestamp },
});
@@ -616,7 +617,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
);
const mediaSkipBackward = useCallback(() => {
logFn.debug('Media skip backward', {
logFn.debug(logMsg[LogCategory.PLAYER].mediaSkipBackward, {
category: LogCategory.PLAYER,
});
@@ -624,7 +625,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
}, [storeActions]);
const mediaSkipForward = useCallback(() => {
logFn.debug('Media skip forward', {
logFn.debug(logMsg[LogCategory.PLAYER].mediaSkipForward, {
category: LogCategory.PLAYER,
});
@@ -633,7 +634,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
const setQueue = useCallback(
(data: Song[], index?: number, position?: number) => {
logFn.debug('Set queue', {
logFn.debug(logMsg[LogCategory.PLAYER].setQueue, {
category: LogCategory.PLAYER,
meta: {
data: data.length,
@@ -649,7 +650,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
const setSpeed = useCallback(
(speed: number) => {
logFn.debug('Set speed', {
logFn.debug(logMsg[LogCategory.PLAYER].setSpeed, {
category: LogCategory.PLAYER,
meta: { speed },
});
@@ -660,7 +661,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
);
const mediaToggleMute = useCallback(() => {
logFn.debug('Media toggle mute', {
logFn.debug(logMsg[LogCategory.PLAYER].mediaToggleMute, {
category: LogCategory.PLAYER,
});
@@ -668,7 +669,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
}, [storeActions]);
const mediaTogglePlayPause = useCallback(() => {
logFn.debug('Media toggle play pause', {
logFn.debug(logMsg[LogCategory.PLAYER].mediaTogglePlayPause, {
category: LogCategory.PLAYER,
});
@@ -677,7 +678,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
const moveSelectedTo = useCallback(
(items: QueueSong[], edge: 'bottom' | 'top', uniqueId: string) => {
logFn.debug('Moved selected to', {
logFn.debug(logMsg[LogCategory.PLAYER].moveSelectedTo, {
category: LogCategory.PLAYER,
meta: { edge, items, uniqueId },
});
@@ -689,7 +690,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
const moveSelectedToBottom = useCallback(
(items: QueueSong[]) => {
logFn.debug('Moved selected to bottom', {
logFn.debug(logMsg[LogCategory.PLAYER].moveSelectedToBottom, {
category: LogCategory.PLAYER,
meta: { items },
});
@@ -701,7 +702,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
const moveSelectedToNext = useCallback(
(items: QueueSong[]) => {
logFn.debug('Moved selected to next', {
logFn.debug(logMsg[LogCategory.PLAYER].moveSelectedToNext, {
category: LogCategory.PLAYER,
meta: { items },
});
@@ -713,7 +714,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
const moveSelectedToTop = useCallback(
(items: QueueSong[]) => {
logFn.debug('Moved selected to top', {
logFn.debug(logMsg[LogCategory.PLAYER].moveSelectedToTop, {
category: LogCategory.PLAYER,
meta: { items },
});
@@ -725,7 +726,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
const setVolume = useCallback(
(volume: number) => {
logFn.debug('Set volume', {
logFn.debug(logMsg[LogCategory.PLAYER].setVolume, {
category: LogCategory.PLAYER,
meta: { volume },
});
@@ -737,7 +738,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
const setRepeat = useCallback(
(repeat: PlayerRepeat) => {
logFn.debug('Set repeat', {
logFn.debug(logMsg[LogCategory.PLAYER].setRepeat, {
category: LogCategory.PLAYER,
meta: { repeat },
});
@@ -749,7 +750,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
const setShuffle = useCallback(
(shuffle: PlayerShuffle) => {
logFn.debug('Set shuffle', {
logFn.debug(logMsg[LogCategory.PLAYER].setShuffle, {
category: LogCategory.PLAYER,
meta: { shuffle },
});
@@ -760,7 +761,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
);
const shuffle = useCallback(() => {
logFn.debug('Shuffle', {
logFn.debug(logMsg[LogCategory.PLAYER].shuffle, {
category: LogCategory.PLAYER,
});
@@ -768,7 +769,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
}, [storeActions]);
const shuffleAll = useCallback(() => {
logFn.debug('Shuffle all', {
logFn.debug(logMsg[LogCategory.PLAYER].shuffleAll, {
category: LogCategory.PLAYER,
});
@@ -777,7 +778,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
const shuffleSelected = useCallback(
(items: QueueSong[]) => {
logFn.debug('Shuffle selected', {
logFn.debug(logMsg[LogCategory.PLAYER].shuffleSelected, {
category: LogCategory.PLAYER,
meta: { items },
});
@@ -788,7 +789,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
);
const toggleRepeat = useCallback(() => {
logFn.debug('Toggle repeat', {
logFn.debug(logMsg[LogCategory.PLAYER].toggleRepeat, {
category: LogCategory.PLAYER,
});
@@ -796,7 +797,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
}, [storeActions]);
const toggleShuffle = useCallback(() => {
logFn.debug('Toggle shuffle', {
logFn.debug(logMsg[LogCategory.PLAYER].toggleShuffle, {
category: LogCategory.PLAYER,
});
@@ -16,6 +16,7 @@ import {
useSettingsStore,
} from '/@/renderer/store';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { logMsg } from '/@/renderer/utils/logger-message';
import { shuffleInPlace } from '/@/renderer/utils/shuffle';
import { hasFeature } from '/@/shared/api/utils';
import { Played, Song, SongListSort, SortOrder } from '/@/shared/types/domain-types';
@@ -62,7 +63,7 @@ export const useAutoDJ = () => {
return;
}
logFn.debug('Auto play triggered', {
logFn.debug(logMsg[LogCategory.PLAYER].autoPlayTriggered, {
category: LogCategory.PLAYER,
meta: { remaining: properties.remaining, songId: properties.song?.id },
});
@@ -206,7 +207,7 @@ export const useAutoDJ = () => {
songCount: songsToAdd.length,
});
} catch (error) {
logFn.error('Auto play failed', {
logFn.error(logMsg[LogCategory.PLAYER].autoPlayFailed, {
category: LogCategory.PLAYER,
meta: { error: (error as Error).message, songId: properties.song?.id },
});
@@ -12,6 +12,7 @@ import {
useTimestampStoreBase,
} from '/@/renderer/store';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { logMsg } from '/@/renderer/utils/logger-message';
import { LibraryItem, QueueSong, ServerType } from '/@/shared/types/domain-types';
import { PlayerStatus } from '/@/shared/types/types';
@@ -130,7 +131,7 @@ export const useScrobble = () => {
},
{
onSuccess: () => {
logFn.debug('Scrobbled a timeupdate event', {
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledTimeupdate, {
category: LogCategory.SCROBBLE,
meta: {
id: currentSong.id,
@@ -172,7 +173,7 @@ export const useScrobble = () => {
},
{
onSuccess: () => {
logFn.debug('Scrobbled a submission event', {
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledSubmission, {
category: LogCategory.SCROBBLE,
meta: {
id: currentSong.id,
@@ -256,7 +257,7 @@ export const useScrobble = () => {
},
{
onSuccess: () => {
logFn.debug('Scrobbled a start event', {
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledStart, {
category: LogCategory.SCROBBLE,
meta: {
id: currentSong.id,
@@ -318,7 +319,7 @@ export const useScrobble = () => {
},
{
onSuccess: () => {
logFn.debug('Scrobbled a timeupdate event', {
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledTimeupdate, {
category: LogCategory.SCROBBLE,
meta: {
id: currentSong.id,
@@ -366,7 +367,7 @@ export const useScrobble = () => {
},
{
onSuccess: () => {
logFn.debug('Scrobbled a pause event', {
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledPause, {
category: LogCategory.SCROBBLE,
meta: {
id: currentSong.id,
@@ -392,7 +393,7 @@ export const useScrobble = () => {
},
{
onSuccess: () => {
logFn.debug('Scrobbled an unpause event', {
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledUnpause, {
category: LogCategory.SCROBBLE,
meta: {
id: currentSong.id,
@@ -435,7 +436,7 @@ export const useScrobble = () => {
},
{
onSuccess: () => {
logFn.debug('Scrobbled a start event', {
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledStart, {
category: LogCategory.SCROBBLE,
meta: {
id: currentSong.id,
+2 -1
View File
@@ -5,6 +5,7 @@ import { queryKeys } from '/@/renderer/api/query-keys';
import { folderQueries } from '/@/renderer/features/folders/api/folder-api';
import { PlayerFilter, useSettingsStore } from '/@/renderer/store';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { logMsg } from '/@/renderer/utils/logger-message';
import { sortSongList } from '/@/shared/api/utils';
import {
PlaylistSongListQuery,
@@ -433,7 +434,7 @@ export const filterSongsByPlayerFilters = (songs: Song[], filters: PlayerFilter[
});
if (filteredSongs.length > 0) {
logFn.debug('Player filters applied', {
logFn.debug(logMsg[LogCategory.PLAYER].playerFiltersApplied, {
category: LogCategory.PLAYER,
meta: {
filteredCount: filteredSongs.length,
@@ -1,635 +0,0 @@
import type { RowComponentProps } from 'react-window-v2';
import { useSuspenseQuery } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router';
import { getItemImageUrl } from '/@/renderer/components/item-image/item-image';
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { usePlaylistSongListFilters } from '/@/renderer/features/playlists/hooks/use-playlist-song-list-filters';
import { applyClientSideSongFilters } from '/@/renderer/features/playlists/hooks/use-playlist-track-list';
import {
ArtistMultiSelectRow,
GenreMultiSelectRow,
} from '/@/renderer/features/shared/components/multi-select-rows';
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
import { useCurrentServer } from '/@/renderer/store';
import { useAppStore, useAppStoreActions } from '/@/renderer/store/app.store';
import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group';
import {
VirtualMultiSelect,
type VirtualMultiSelectOption,
} from '/@/shared/components/multi-select/virtual-multi-select';
import { NumberInput } from '/@/shared/components/number-input/number-input';
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text';
import { useDebouncedCallback } from '/@/shared/hooks/use-debounced-callback';
import { LibraryItem, Song } from '/@/shared/types/domain-types';
interface BooleanSegmentFilterProps {
label: string;
onChange: (value: boolean | null) => void;
segmentData: Array<{ label: string; value: string }>;
value: boolean | null | undefined;
}
function booleanToSegmentValue(value: boolean | null | undefined): string {
if (value === true) return 'true';
if (value === false) return 'false';
return 'none';
}
function segmentValueToBoolean(value: string): boolean | null {
if (value === 'true') return true;
if (value === 'false') return false;
return null;
}
const BooleanSegmentFilter = ({
label,
onChange,
segmentData,
value,
}: BooleanSegmentFilterProps) => (
<Stack gap="xs">
<Text size="sm" weight={500}>
{label}
</Text>
<SegmentedControl
data={segmentData}
onChange={(v) => onChange(segmentValueToBoolean(v))}
size="sm"
value={booleanToSegmentValue(value)}
w="100%"
/>
</Stack>
);
interface MultiSelectFilterOption {
albumCount: null | number;
imageUrl: string | undefined;
label: string;
songCount: number;
value: string;
}
interface MultiSelectFilterProps {
displayCountType?: 'song';
height: number;
label: React.ReactNode;
onChange: (value: null | string[]) => void;
options: MultiSelectFilterOption[];
RowComponent: (props: RowComponentProps<MultiSelectRowContext>) => React.ReactElement;
singleSelect: boolean;
value: string[];
}
type MultiSelectRowContext = {
disabled?: boolean;
displayCountType?: 'album' | 'song';
focusedIndex: null | number;
onToggle: (value: string) => void;
options: VirtualMultiSelectOption<MultiSelectFilterOption>[];
value: string[];
};
const MultiSelectFilter = ({
displayCountType = 'song',
height,
label,
onChange,
options,
RowComponent,
singleSelect,
value,
}: MultiSelectFilterProps) => (
<VirtualMultiSelect
displayCountType={displayCountType}
height={height}
label={label}
onChange={onChange}
options={options}
RowComponent={RowComponent}
singleSelect={singleSelect}
value={value}
/>
);
interface YearRangeFilterProps {
fromYearLabel: string;
maxYear: number | undefined;
minYear: number | undefined;
onMaxYear: (e: number | string) => void;
onMinYear: (e: number | string) => void;
toYearLabel: string;
}
const YearRangeFilter = ({
fromYearLabel,
maxYear,
minYear,
onMaxYear,
onMinYear,
toYearLabel,
}: YearRangeFilterProps) => (
<Group gap="sm" wrap="nowrap">
<NumberInput
hideControls={false}
label={fromYearLabel}
max={5000}
min={0}
onChange={(e) => onMinYear(e)}
style={{ flex: 1 }}
value={minYear != null ? minYear : ''}
/>
<NumberInput
hideControls={false}
label={toYearLabel}
max={5000}
min={0}
onChange={(e) => onMaxYear(e)}
style={{ flex: 1 }}
value={maxYear != null ? maxYear : ''}
/>
</Group>
);
interface MultiSelectFilterLabelProps {
andOrValue: 'and' | 'or';
entityLabel: string;
filterMultipleLabel: string;
filterSingleLabel: string;
matchAndLabel: string;
matchOrLabel: string;
onAndOrChange: (value: 'and' | 'or') => void;
onSingleMultiChange: (value: string) => void;
showAndOr: boolean;
singleMultiValue: 'multi' | 'single';
}
const MultiSelectFilterLabel = ({
andOrValue,
entityLabel,
filterMultipleLabel,
filterSingleLabel,
matchAndLabel,
matchOrLabel,
onAndOrChange,
onSingleMultiChange,
showAndOr,
singleMultiValue,
}: MultiSelectFilterLabelProps) => (
<Group gap="xs" justify="space-between" w="100%">
<Text fw={500} size="sm">
{entityLabel}
</Text>
<Group gap="xs">
{showAndOr && (
<SegmentedControl
data={[
{ label: matchAndLabel, value: 'and' },
{ label: matchOrLabel, value: 'or' },
]}
onChange={(value) => onAndOrChange(value === 'or' ? 'or' : 'and')}
size="xs"
value={andOrValue}
/>
)}
<SegmentedControl
data={[
{ label: filterSingleLabel, value: 'single' },
{ label: filterMultipleLabel, value: 'multi' },
]}
onChange={onSingleMultiChange}
size="xs"
value={singleMultiValue}
/>
</Group>
</Group>
);
export const ClientSideSongFilters = () => {
const { t } = useTranslation();
const { playlistId } = useParams() as { playlistId: string };
const server = useCurrentServer();
const {
query,
setAlbumArtistIds,
setAlbumArtistIdsMode,
setArtistIds,
setArtistIdsMode,
setFavorite,
setGenreId,
setGenreIdsMode,
setHasRating,
setMaxYear,
setMinYear,
} = usePlaylistSongListFilters();
const playlistSongsQuery = useSuspenseQuery(
playlistsQueries.songList({
query: { id: playlistId },
serverId: server?.id,
}),
);
const albumArtistSelectMode = useAppStore((state) => state.albumArtistSelectMode);
const artistSelectMode = useAppStore((state) => state.artistSelectMode);
const genreSelectMode = useAppStore((state) => state.genreSelectMode);
const { setAlbumArtistSelectMode, setArtistSelectMode, setGenreSelectMode } =
useAppStoreActions();
const songs = useMemo(() => {
return (playlistSongsQuery.data?.items ?? []) as Song[];
}, [playlistSongsQuery.data]);
const filteredSongs = useMemo(
() => applyClientSideSongFilters(songs, query as Record<string, unknown>),
[songs, query],
);
const songsForAlbumArtistOptions = useMemo(() => {
const idsMode =
(query[FILTER_KEYS.SONG.ALBUM_ARTIST_IDS_MODE] as 'and' | 'or' | undefined) ?? 'and';
const useFilteredResult = albumArtistSelectMode === 'multi' && idsMode === 'and';
if (!useFilteredResult) {
const queryWithoutAlbumArtist = {
...query,
[FILTER_KEYS.SONG.ALBUM_ARTIST_IDS]: undefined,
} as Record<string, unknown>;
return applyClientSideSongFilters(songs, queryWithoutAlbumArtist);
}
return filteredSongs;
}, [albumArtistSelectMode, filteredSongs, query, songs]);
const songsForArtistOptions = useMemo(() => {
const idsMode =
(query[FILTER_KEYS.SONG.ARTIST_IDS_MODE] as 'and' | 'or' | undefined) ?? 'and';
const useFilteredResult = artistSelectMode === 'multi' && idsMode === 'and';
if (!useFilteredResult) {
const queryWithoutArtist = {
...query,
[FILTER_KEYS.SONG.ARTIST_IDS]: undefined,
} as Record<string, unknown>;
return applyClientSideSongFilters(songs, queryWithoutArtist);
}
return filteredSongs;
}, [artistSelectMode, filteredSongs, query, songs]);
const songsForGenreOptions = useMemo(() => {
const idsMode =
(query[FILTER_KEYS.SONG.GENRE_ID_MODE] as 'and' | 'or' | undefined) ?? 'and';
const useFilteredResult = genreSelectMode === 'multi' && idsMode === 'and';
if (!useFilteredResult) {
const queryWithoutGenre = {
...query,
[FILTER_KEYS.SONG.GENRE_ID]: undefined,
} as Record<string, unknown>;
return applyClientSideSongFilters(songs, queryWithoutGenre);
}
return filteredSongs;
}, [filteredSongs, genreSelectMode, query, songs]);
const albumArtistOptions = useMemo(() => {
const byId = new Map<
string,
{ id: string; imageUrl: string | undefined; name: string; songCount: number }
>();
for (const song of songsForAlbumArtistOptions) {
for (const artist of song.albumArtists ?? []) {
if (!artist.id) continue;
const existing = byId.get(artist.id);
if (existing) {
existing.songCount += 1;
} else {
byId.set(artist.id, {
id: artist.id,
imageUrl:
artist.imageUrl ??
getItemImageUrl({
id: artist.id,
itemType: LibraryItem.ALBUM_ARTIST,
type: 'table',
}),
name: artist.name,
songCount: 1,
});
}
}
}
return Array.from(byId.values())
.sort((a, b) => a.name.localeCompare(b.name))
.map((a) => ({
albumCount: null as null | number,
imageUrl: a.imageUrl,
label: a.name,
songCount: a.songCount,
value: a.id,
}));
}, [songsForAlbumArtistOptions]);
const artistOptions = useMemo(() => {
const byId = new Map<
string,
{ id: string; imageUrl: string | undefined; name: string; songCount: number }
>();
for (const song of songsForArtistOptions) {
for (const artist of song.artists ?? []) {
if (!artist.id) continue;
const existing = byId.get(artist.id);
if (existing) {
existing.songCount += 1;
} else {
byId.set(artist.id, {
id: artist.id,
imageUrl:
artist.imageUrl ??
getItemImageUrl({
id: artist.id,
itemType: LibraryItem.ARTIST,
type: 'table',
}),
name: artist.name,
songCount: 1,
});
}
}
}
return Array.from(byId.values())
.sort((a, b) => a.name.localeCompare(b.name))
.map((a) => ({
albumCount: null as null | number,
imageUrl: a.imageUrl,
label: a.name,
songCount: a.songCount,
value: a.id,
}));
}, [songsForArtistOptions]);
const genreOptions = useMemo(() => {
const byId = new Map<string, { id: string; name: string; songCount: number }>();
for (const song of songsForGenreOptions) {
for (const genre of song.genres ?? []) {
if (!genre.id) continue;
const existing = byId.get(genre.id);
if (existing) {
existing.songCount += 1;
} else {
byId.set(genre.id, {
id: genre.id,
name: genre.name,
songCount: 1,
});
}
}
}
return Array.from(byId.values())
.sort((a, b) => a.name.localeCompare(b.name))
.map((g) => ({
albumCount: null as null | number,
imageUrl: undefined,
label: g.name,
songCount: g.songCount,
value: g.id,
}));
}, [songsForGenreOptions]);
const segmentedControlData = useMemo(
() => [
{ label: t('common.none', { postProcess: 'titleCase' }), value: 'none' },
{ label: t('common.yes', { postProcess: 'titleCase' }), value: 'true' },
{ label: t('common.no', { postProcess: 'titleCase' }), value: 'false' },
],
[t],
);
const handleMinYear = useMemo(
() => (e: number | string) => {
if (e === '' || e === null || e === undefined) {
setMinYear(null);
return;
}
const year = typeof e === 'number' ? e : Number(e);
setMinYear(!isNaN(year) && isFinite(year) && year > 0 ? year : null);
},
[setMinYear],
);
const handleMaxYear = useMemo(
() => (e: number | string) => {
if (e === '' || e === null || e === undefined) {
setMaxYear(null);
return;
}
const year = typeof e === 'number' ? e : Number(e);
setMaxYear(!isNaN(year) && isFinite(year) && year > 0 ? year : null);
},
[setMaxYear],
);
const debouncedHandleMinYear = useDebouncedCallback(handleMinYear, 300);
const debouncedHandleMaxYear = useDebouncedCallback(handleMaxYear, 300);
const selectedGenreIds = useMemo(
() => (query[FILTER_KEYS.SONG.GENRE_ID] as string[] | undefined) ?? [],
[query],
);
const handleGenreSelectModeChange = useCallback(
(value: string) => {
const newMode = value as 'multi' | 'single';
setGenreSelectMode(newMode);
if (newMode === 'single' && selectedGenreIds.length > 1) {
setGenreId([selectedGenreIds[0]]);
}
},
[selectedGenreIds, setGenreId, setGenreSelectMode],
);
const genreIdsMode =
(query[FILTER_KEYS.SONG.GENRE_ID_MODE] as 'and' | 'or' | undefined) ?? 'and';
const handleGenreChange = useCallback(
(e: null | string[]) => {
if (e && e.length > 0) {
setGenreId(e);
} else {
setGenreId(null);
}
},
[setGenreId],
);
const selectedArtistIds = useMemo(
() => (query[FILTER_KEYS.SONG.ARTIST_IDS] as string[] | undefined) ?? [],
[query],
);
const handleArtistSelectModeChange = useCallback(
(value: string) => {
const newMode = value as 'multi' | 'single';
setArtistSelectMode(newMode);
if (newMode === 'single' && selectedArtistIds.length > 1) {
setArtistIds([selectedArtistIds[0]]);
}
},
[selectedArtistIds, setArtistIds, setArtistSelectMode],
);
const artistIdsMode =
(query[FILTER_KEYS.SONG.ARTIST_IDS_MODE] as 'and' | 'or' | undefined) ?? 'and';
const handleArtistChange = useCallback(
(e: null | string[]) => {
if (e && e.length > 0) {
setArtistIds(e);
} else {
setArtistIds(null);
}
},
[setArtistIds],
);
const selectedAlbumArtistIds = useMemo(
() => (query[FILTER_KEYS.SONG.ALBUM_ARTIST_IDS] as string[] | undefined) ?? [],
[query],
);
const handleAlbumArtistSelectModeChange = useCallback(
(value: string) => {
const newMode = value as 'multi' | 'single';
setAlbumArtistSelectMode(newMode);
if (newMode === 'single' && selectedAlbumArtistIds.length > 1) {
setAlbumArtistIds([selectedAlbumArtistIds[0]]);
}
},
[selectedAlbumArtistIds, setAlbumArtistIds, setAlbumArtistSelectMode],
);
const albumArtistIdsMode =
(query[FILTER_KEYS.SONG.ALBUM_ARTIST_IDS_MODE] as 'and' | 'or' | undefined) ?? 'and';
const handleAlbumArtistChange = useCallback(
(e: null | string[]) => {
if (e && e.length > 0) {
setAlbumArtistIds(e);
} else {
setAlbumArtistIds(null);
}
},
[setAlbumArtistIds],
);
const queryFavorite = query[FILTER_KEYS.SONG.FAVORITE] as boolean | undefined;
const queryHasRating = query[FILTER_KEYS.SONG.HAS_RATING] as boolean | undefined;
const queryMinYear = query[FILTER_KEYS.SONG.MIN_YEAR] as number | undefined;
const queryMaxYear = query[FILTER_KEYS.SONG.MAX_YEAR] as number | undefined;
const matchAndLabel = t('filter.matchAnd', { postProcess: 'titleCase' });
const matchOrLabel = t('filter.matchOr', { postProcess: 'titleCase' });
const filterSingleLabel = t('common.filter_single', { postProcess: 'titleCase' });
const filterMultipleLabel = t('common.filter_multiple', { postProcess: 'titleCase' });
return (
<Stack px="md" py="md">
<BooleanSegmentFilter
label={t('filter.isFavorited', { postProcess: 'sentenceCase' })}
onChange={setFavorite}
segmentData={segmentedControlData}
value={queryFavorite}
/>
<Stack gap="xs" mt="md">
<BooleanSegmentFilter
label={t('filter.isRated', { postProcess: 'sentenceCase' })}
onChange={setHasRating}
segmentData={segmentedControlData}
value={queryHasRating}
/>
</Stack>
<Divider my="md" />
<MultiSelectFilter
height={300}
label={
<MultiSelectFilterLabel
andOrValue={artistIdsMode}
entityLabel={t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
filterMultipleLabel={filterMultipleLabel}
filterSingleLabel={filterSingleLabel}
matchAndLabel={matchAndLabel}
matchOrLabel={matchOrLabel}
onAndOrChange={setArtistIdsMode}
onSingleMultiChange={handleArtistSelectModeChange}
showAndOr={artistSelectMode === 'multi'}
singleMultiValue={artistSelectMode}
/>
}
onChange={handleArtistChange}
options={artistOptions}
RowComponent={ArtistMultiSelectRow}
singleSelect={artistSelectMode === 'single'}
value={selectedArtistIds}
/>
<Divider my="md" />
<MultiSelectFilter
height={300}
label={
<MultiSelectFilterLabel
andOrValue={albumArtistIdsMode}
entityLabel={t('entity.albumArtist', {
count: 2,
postProcess: 'sentenceCase',
})}
filterMultipleLabel={filterMultipleLabel}
filterSingleLabel={filterSingleLabel}
matchAndLabel={matchAndLabel}
matchOrLabel={matchOrLabel}
onAndOrChange={setAlbumArtistIdsMode}
onSingleMultiChange={handleAlbumArtistSelectModeChange}
showAndOr={albumArtistSelectMode === 'multi'}
singleMultiValue={albumArtistSelectMode}
/>
}
onChange={handleAlbumArtistChange}
options={albumArtistOptions}
RowComponent={ArtistMultiSelectRow}
singleSelect={albumArtistSelectMode === 'single'}
value={selectedAlbumArtistIds}
/>
<Divider my="md" />
<MultiSelectFilter
height={220}
label={
<MultiSelectFilterLabel
andOrValue={genreIdsMode}
entityLabel={t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
filterMultipleLabel={filterMultipleLabel}
filterSingleLabel={filterSingleLabel}
matchAndLabel={matchAndLabel}
matchOrLabel={matchOrLabel}
onAndOrChange={setGenreIdsMode}
onSingleMultiChange={handleGenreSelectModeChange}
showAndOr={genreSelectMode === 'multi'}
singleMultiValue={genreSelectMode}
/>
}
onChange={handleGenreChange}
options={genreOptions}
RowComponent={GenreMultiSelectRow}
singleSelect={genreSelectMode === 'single'}
value={selectedGenreIds}
/>
<Divider my="md" />
<YearRangeFilter
fromYearLabel={t('filter.fromYear', { postProcess: 'titleCase' })}
maxYear={queryMaxYear}
minYear={queryMinYear}
onMaxYear={debouncedHandleMaxYear}
onMinYear={debouncedHandleMinYear}
toYearLabel={t('filter.toYear', { postProcess: 'titleCase' })}
/>
</Stack>
);
};
@@ -15,7 +15,6 @@ import { useListContext } from '/@/renderer/context/list-context';
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { usePlaylistSongListFilters } from '/@/renderer/features/playlists/hooks/use-playlist-song-list-filters';
import { applyClientSideSongFilters } from '/@/renderer/features/playlists/hooks/use-playlist-track-list';
import { type PlaylistAlbumRow, playlistSongsToAlbums } from '/@/renderer/features/playlists/utils';
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
import { searchLibraryItems } from '/@/renderer/features/shared/utils';
@@ -41,25 +40,18 @@ export const PlaylistDetailAlbumView = ({ data }: { data: PlaylistSongListRespon
const { searchTerm } = useSearchTermFilter();
const { query } = usePlaylistSongListFilters();
const filteredAndSortedSongs = useMemo(() => {
const raw = data?.items ?? [];
const filtered = applyClientSideSongFilters(raw, query as Record<string, unknown>);
const searched = searchTerm?.trim()
? searchLibraryItems(filtered, searchTerm, LibraryItem.SONG)
: filtered;
return sortSongList(
searched,
const sortedAlbums = useMemo(() => {
let songs = data?.items ?? [];
if (searchTerm?.trim()) {
songs = searchLibraryItems(songs, searchTerm, LibraryItem.SONG);
}
const sortedSongs = sortSongList(
songs,
(query.sortBy as SongListSort) ?? SongListSort.ID,
(query.sortOrder as SortOrder) ?? SortOrder.ASC,
);
}, [data?.items, query, searchTerm]);
const sortedAlbums = useMemo(
() => playlistSongsToAlbums(filteredAndSortedSongs),
[filteredAndSortedSongs],
);
return playlistSongsToAlbums(sortedSongs);
}, [data?.items, searchTerm, query.sortBy, query.sortOrder]);
const isPaginated = pagination === ListPaginationType.PAGINATED;
const totalAlbumCount = sortedAlbums.length;
@@ -127,8 +119,8 @@ export const PlaylistDetailAlbumView = ({ data }: { data: PlaylistSongListRespon
}, [setItemCount, totalAlbumCount]);
useEffect(() => {
setListData?.(filteredAndSortedSongs);
}, [filteredAndSortedSongs, setListData]);
setListData?.(data?.items ?? []);
}, [data?.items, setListData]);
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({ enabled: true });
const { handleColumnReordered } = useItemListColumnReorder({
@@ -1,6 +1,6 @@
import { openContextModal } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router';
@@ -13,17 +13,12 @@ import {
import { useListContext } from '/@/renderer/context/list-context';
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { ClientSideSongFilters } from '/@/renderer/features/playlists/components/client-side-song-filters';
import { usePlaylistSongListFilters } from '/@/renderer/features/playlists/hooks/use-playlist-song-list-filters';
import { FilterButton } from '/@/renderer/features/shared/components/filter-button';
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
import { ListDisplayTypeToggleButton } from '/@/renderer/features/shared/components/list-display-type-toggle-button';
import { isFilterValueSet } from '/@/renderer/features/shared/components/list-filters';
import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button';
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
import { MoreButton } from '/@/renderer/features/shared/components/more-button';
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
import { useContainerQuery } from '/@/renderer/hooks';
import {
PlaylistTarget,
@@ -37,9 +32,7 @@ import { Divider } from '/@/shared/components/divider/divider';
import { Flex } from '/@/shared/components/flex/flex';
import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
import { Modal } from '/@/shared/components/modal/modal';
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
import { useDisclosure } from '/@/shared/hooks/use-disclosure';
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
import { LibraryItem, SongListSort, SortOrder } from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types';
@@ -48,69 +41,6 @@ interface PlaylistDetailSongListHeaderFiltersProps {
isSmartPlaylist?: boolean;
}
const PlaylistSongListFiltersModal = () => {
const { t } = useTranslation();
const { isSidebarOpen, setIsSidebarOpen } = useListContext();
const { clear, query } = usePlaylistSongListFilters();
const [isOpen, handlers] = useDisclosure(false);
const hasActiveFilters = useMemo(() => {
return Boolean(
isFilterValueSet(query[FILTER_KEYS.SONG.ALBUM_ARTIST_IDS]) ||
isFilterValueSet(query[FILTER_KEYS.SONG.ARTIST_IDS]) ||
query[FILTER_KEYS.SONG.FAVORITE] !== undefined ||
isFilterValueSet(query[FILTER_KEYS.SONG.GENRE_ID]) ||
query[FILTER_KEYS.SONG.HAS_RATING] !== undefined ||
query[FILTER_KEYS.SONG.MAX_YEAR] !== undefined ||
query[FILTER_KEYS.SONG.MIN_YEAR] !== undefined,
);
}, [query]);
const handlePin = () => {
setIsSidebarOpen?.(!isSidebarOpen);
};
const canPin = Boolean(setIsSidebarOpen);
return (
<>
<FilterButton isActive={hasActiveFilters} onClick={handlers.toggle} />
<Modal
handlers={handlers}
opened={isOpen}
size="lg"
styles={{
content: {
height: '100%',
maxHeight: '640px',
maxWidth: 'var(--theme-content-max-width)',
width: '100%',
},
}}
title={
<Group justify="space-between" style={{ paddingRight: '3rem', width: '100%' }}>
<Group>
{canPin && (
<ActionIcon
icon={isSidebarOpen ? 'unpin' : 'pin'}
onClick={handlePin}
variant="subtle"
/>
)}
{t('common.filters', { postProcess: 'sentenceCase' })}
</Group>
<Button onClick={clear} size="compact-sm" variant="subtle">
{t('common.reset', { postProcess: 'sentenceCase' })}
</Button>
</Group>
}
>
<ClientSideSongFilters />
</Modal>
</>
);
};
export const PlaylistDetailSongListHeaderFilters = ({
isSmartPlaylist,
}: PlaylistDetailSongListHeaderFiltersProps) => {
@@ -184,8 +114,6 @@ export const PlaylistDetailSongListHeaderFilters = ({
disabled={isEditMode}
listKey={ItemListKey.PLAYLIST_SONG}
/>
<Divider orientation="vertical" />
<PlaylistSongListFiltersModal />
<ListRefreshButton disabled={isEditMode} listKey={listKey} />
<MoreButton onClick={handleMore} />
</Group>
@@ -5,25 +5,17 @@ import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-searc
import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter';
import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter';
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
import { useAppStore } from '/@/renderer/store/app.store';
import {
parseArrayParam,
parseBooleanParam,
parseCustomFiltersParam,
parseIntParam,
setMultipleSearchParams,
setSearchParam,
} from '/@/renderer/utils/query-params';
import { SongListSort, SortOrder } from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types';
export const usePlaylistSongListFilters = () => {
const albumArtistIdsMode = useAppStore((state) => state.albumArtistIdsMode);
const artistIdsMode = useAppStore((state) => state.artistIdsMode);
const genreIdsMode = useAppStore((state) => state.genreIdsMode);
const setAlbumArtistIdsModeStore = useAppStore((state) => state.actions.setAlbumArtistIdsMode);
const setArtistIdsModeStore = useAppStore((state) => state.actions.setArtistIdsMode);
const setGenreIdsModeStore = useAppStore((state) => state.actions.setGenreIdsMode);
const { sortBy } = useSortByFilter<SongListSort>(SongListSort.ID, ItemListKey.PLAYLIST_SONG);
const { sortOrder } = useSortOrderFilter(SortOrder.ASC, ItemListKey.PLAYLIST_SONG);
@@ -32,8 +24,8 @@ export const usePlaylistSongListFilters = () => {
const [searchParams, setSearchParams] = useSearchParams();
const albumArtistIds = useMemo(
() => parseArrayParam(searchParams, FILTER_KEYS.SONG.ALBUM_ARTIST_IDS),
const albumIds = useMemo(
() => parseArrayParam(searchParams, FILTER_KEYS.SONG.ALBUM_IDS),
[searchParams],
);
@@ -62,22 +54,16 @@ export const usePlaylistSongListFilters = () => {
[searchParams],
);
const hasRating = useMemo(
() => parseBooleanParam(searchParams, FILTER_KEYS.SONG.HAS_RATING),
[searchParams],
);
const custom = useMemo(
() => parseCustomFiltersParam(searchParams, FILTER_KEYS.SONG._CUSTOM),
[searchParams],
);
const setAlbumArtistIds = useCallback(
const setAlbumIds = useCallback(
(value: null | string[]) => {
setSearchParams(
(prev) => setSearchParam(prev, FILTER_KEYS.SONG.ALBUM_ARTIST_IDS, value),
{ replace: true },
);
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.ALBUM_IDS, value), {
replace: true,
});
},
[setSearchParams],
);
@@ -127,30 +113,6 @@ export const usePlaylistSongListFilters = () => {
[setSearchParams],
);
const setHasRating = useCallback(
(value: boolean | null) => {
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.HAS_RATING, value), {
replace: true,
});
},
[setSearchParams],
);
const setAlbumArtistIdsMode = useCallback(
(value: 'and' | 'or') => setAlbumArtistIdsModeStore(value),
[setAlbumArtistIdsModeStore],
);
const setArtistIdsMode = useCallback(
(value: 'and' | 'or') => setArtistIdsModeStore(value),
[setArtistIdsModeStore],
);
const setGenreIdsMode = useCallback(
(value: 'and' | 'or') => setGenreIdsModeStore(value),
[setGenreIdsModeStore],
);
const setCustom = useCallback(
(value: null | Record<string, any>) => {
setSearchParams(
@@ -179,74 +141,26 @@ export const usePlaylistSongListFilters = () => {
[setSearchParams],
);
const clear = useCallback(() => {
setSearchParams(
(prev) =>
setMultipleSearchParams(
prev,
{
[FILTER_KEYS.SONG._CUSTOM]: null,
[FILTER_KEYS.SONG.ALBUM_ARTIST_IDS]: null,
[FILTER_KEYS.SONG.ARTIST_IDS]: null,
[FILTER_KEYS.SONG.FAVORITE]: null,
[FILTER_KEYS.SONG.GENRE_ID]: null,
[FILTER_KEYS.SONG.HAS_RATING]: null,
[FILTER_KEYS.SONG.MAX_YEAR]: null,
[FILTER_KEYS.SONG.MIN_YEAR]: null,
},
new Set([FILTER_KEYS.SONG._CUSTOM]),
),
{ replace: true },
);
}, [setSearchParams]);
const query = useMemo(
() => ({
[FILTER_KEYS.SHARED.SEARCH_TERM]: searchTerm ?? undefined,
[FILTER_KEYS.SHARED.SORT_BY]: sortBy ?? undefined,
[FILTER_KEYS.SHARED.SORT_ORDER]: sortOrder ?? undefined,
[FILTER_KEYS.SONG._CUSTOM]: custom ?? undefined,
[FILTER_KEYS.SONG.ALBUM_ARTIST_IDS]: albumArtistIds ?? undefined,
[FILTER_KEYS.SONG.ALBUM_ARTIST_IDS_MODE]: albumArtistIdsMode,
[FILTER_KEYS.SONG.ARTIST_IDS]: artistIds ?? undefined,
[FILTER_KEYS.SONG.ARTIST_IDS_MODE]: artistIdsMode,
[FILTER_KEYS.SONG.FAVORITE]: favorite ?? undefined,
[FILTER_KEYS.SONG.GENRE_ID]: genreId ?? undefined,
[FILTER_KEYS.SONG.GENRE_ID_MODE]: genreIdsMode,
[FILTER_KEYS.SONG.HAS_RATING]: hasRating ?? undefined,
[FILTER_KEYS.SONG.MAX_YEAR]: maxYear ?? undefined,
[FILTER_KEYS.SONG.MIN_YEAR]: minYear ?? undefined,
}),
[
searchTerm,
sortBy,
sortOrder,
custom,
albumArtistIds,
albumArtistIdsMode,
artistIds,
artistIdsMode,
favorite,
genreId,
genreIdsMode,
hasRating,
maxYear,
minYear,
],
);
const query = {
[FILTER_KEYS.SHARED.SEARCH_TERM]: searchTerm ?? undefined,
[FILTER_KEYS.SHARED.SORT_BY]: sortBy ?? undefined,
[FILTER_KEYS.SHARED.SORT_ORDER]: sortOrder ?? undefined,
[FILTER_KEYS.SONG._CUSTOM]: custom ?? undefined,
[FILTER_KEYS.SONG.ALBUM_IDS]: albumIds ?? undefined,
[FILTER_KEYS.SONG.ARTIST_IDS]: artistIds ?? undefined,
[FILTER_KEYS.SONG.FAVORITE]: favorite ?? undefined,
[FILTER_KEYS.SONG.GENRE_ID]: genreId ?? undefined,
[FILTER_KEYS.SONG.MAX_YEAR]: maxYear ?? undefined,
[FILTER_KEYS.SONG.MIN_YEAR]: minYear ?? undefined,
};
return {
clear,
query,
setAlbumArtistIds,
setAlbumArtistIdsMode,
setAlbumIds,
setArtistIds,
setArtistIdsMode,
setCustom,
setFavorite,
setGenreId,
setGenreIdsMode,
setHasRating,
setMaxYear,
setMinYear,
setSearchTerm,
@@ -3,88 +3,9 @@ import { useEffect, useMemo } from 'react';
import { useListContext } from '/@/renderer/context/list-context';
import { usePlaylistSongListFilters } from '/@/renderer/features/playlists/hooks/use-playlist-song-list-filters';
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
import { searchLibraryItems } from '/@/renderer/features/shared/utils';
import { sortSongList } from '/@/shared/api/utils';
import {
LibraryItem,
PlaylistSongListResponse,
Song,
SongListSort,
SortOrder,
} from '/@/shared/types/domain-types';
export function applyClientSideSongFilters(songs: Song[], query: Record<string, unknown>): Song[] {
let result = songs;
const favorite = query[FILTER_KEYS.SONG.FAVORITE] as boolean | undefined;
if (favorite === true) {
result = result.filter((s) => s.userFavorite === true);
} else if (favorite === false) {
result = result.filter((s) => s.userFavorite === false);
}
const hasRating = query[FILTER_KEYS.SONG.HAS_RATING] as boolean | undefined;
if (hasRating === true) {
result = result.filter((s) => s.userRating != null && s.userRating > 0);
} else if (hasRating === false) {
result = result.filter((s) => s.userRating == null || s.userRating === 0);
}
const albumArtistIdsMode =
(query[FILTER_KEYS.SONG.ALBUM_ARTIST_IDS_MODE] as 'and' | 'or' | undefined) ?? 'and';
const albumArtistIds = query[FILTER_KEYS.SONG.ALBUM_ARTIST_IDS] as string[] | undefined;
if (albumArtistIds?.length) {
if (albumArtistIdsMode === 'and') {
result = result.filter((s) =>
albumArtistIds!.every((id) => s.albumArtists?.some((a) => a.id === id)),
);
} else {
const set = new Set(albumArtistIds);
result = result.filter((s) => s.albumArtists?.some((a) => a.id && set.has(a.id)));
}
}
const artistIdsMode =
(query[FILTER_KEYS.SONG.ARTIST_IDS_MODE] as 'and' | 'or' | undefined) ?? 'and';
const artistIds = query[FILTER_KEYS.SONG.ARTIST_IDS] as string[] | undefined;
if (artistIds?.length) {
if (artistIdsMode === 'and') {
result = result.filter((s) =>
artistIds!.every((id) => s.artists?.some((a) => a.id === id)),
);
} else {
const set = new Set(artistIds);
result = result.filter((s) => s.artists?.some((a) => a.id && set.has(a.id)));
}
}
const genreIdsMode =
(query[FILTER_KEYS.SONG.GENRE_ID_MODE] as 'and' | 'or' | undefined) ?? 'and';
const genreIds = query[FILTER_KEYS.SONG.GENRE_ID] as string[] | undefined;
if (genreIds?.length) {
if (genreIdsMode === 'and') {
result = result.filter((s) =>
genreIds!.every((id) => s.genres?.some((g) => g.id === id)),
);
} else {
const set = new Set(genreIds);
result = result.filter((s) => s.genres?.some((g) => g.id && set.has(g.id)));
}
}
const minYear = query[FILTER_KEYS.SONG.MIN_YEAR] as number | undefined;
if (minYear != null) {
result = result.filter((s) => s.releaseYear != null && s.releaseYear >= minYear);
}
const maxYear = query[FILTER_KEYS.SONG.MAX_YEAR] as number | undefined;
if (maxYear != null) {
result = result.filter((s) => s.releaseYear != null && s.releaseYear <= maxYear);
}
return result;
}
import { LibraryItem, PlaylistSongListResponse, Song } from '/@/shared/types/domain-types';
export function usePlaylistTrackList(data: PlaylistSongListResponse | undefined): {
sortedAndFilteredSongs: Song[];
@@ -96,23 +17,20 @@ export function usePlaylistTrackList(data: PlaylistSongListResponse | undefined)
const sortedAndFilteredSongs = useMemo(() => {
const raw = data?.items ?? [];
const filtered = applyClientSideSongFilters(raw, query as Record<string, unknown>);
const searched = searchTerm
? searchLibraryItems(filtered, searchTerm, LibraryItem.SONG)
: filtered;
return sortSongList(
searched,
(query.sortBy as SongListSort) ?? SongListSort.ID,
(query.sortOrder as SortOrder) ?? SortOrder.ASC,
);
}, [data?.items, query, searchTerm]);
if (searchTerm) {
return searchLibraryItems(raw, searchTerm, LibraryItem.SONG);
}
return sortSongList(raw, query.sortBy, query.sortOrder);
}, [data?.items, searchTerm, query.sortBy, query.sortOrder]);
const totalCount = sortedAndFilteredSongs.length;
useEffect(() => {
setListData?.(sortedAndFilteredSongs);
setItemCount?.(totalCount);
}, [query, searchTerm, setListData, setItemCount, sortedAndFilteredSongs, totalCount]);
}, [sortedAndFilteredSongs, totalCount, setListData, setItemCount]);
return { sortedAndFilteredSongs, totalCount };
}
@@ -6,7 +6,6 @@ import { queryKeys } from '/@/renderer/api/query-keys';
import { useRecentPlaylists } from '/@/renderer/features/playlists/hooks/use-recent-playlists';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { useCurrentServerId } from '/@/renderer/store';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { AddToPlaylistArgs, AddToPlaylistResponse } from '/@/shared/types/domain-types';
export const useAddToPlaylist = (args: MutationHookArgs) => {
@@ -23,17 +22,6 @@ export const useAddToPlaylist = (args: MutationHookArgs) => {
apiClientProps: { serverId: args.apiClientProps.serverId },
});
},
onError: (error, variables) => {
logFn.error('Add to playlist failed', {
category: LogCategory.API,
meta: {
message: error?.message,
playlistId: variables.query.id,
serverId: variables.apiClientProps.serverId,
},
});
options?.onError?.(error);
},
onSuccess: (_data, variables, context) => {
const { apiClientProps } = variables;
const serverId = apiClientProps.serverId;
@@ -4,7 +4,6 @@ import { AxiosError } from 'axios';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { CreatePlaylistArgs, CreatePlaylistResponse } from '/@/shared/types/domain-types';
export const useCreatePlaylist = (args: MutationHookArgs) => {
@@ -18,16 +17,6 @@ export const useCreatePlaylist = (args: MutationHookArgs) => {
apiClientProps: { serverId: args.apiClientProps.serverId },
});
},
onError: (error, variables) => {
logFn.error('Create playlist failed', {
category: LogCategory.API,
meta: {
message: error?.message,
serverId: variables.apiClientProps.serverId,
},
});
options?.onError?.(error);
},
onSuccess: (_args, variables) => {
queryClient.invalidateQueries({
exact: false,
@@ -9,7 +9,6 @@ import {
restorePlaylistQueryData,
} from '/@/renderer/features/playlists/mutations/playlist-optimistic-updates';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { DeletePlaylistArgs, DeletePlaylistResponse } from '/@/shared/types/domain-types';
export const useDeletePlaylist = (args: MutationHookArgs) => {
@@ -25,14 +24,6 @@ export const useDeletePlaylist = (args: MutationHookArgs) => {
});
},
onError: (_error, _variables, context) => {
logFn.error('Delete playlist failed', {
category: LogCategory.API,
meta: {
message: _error?.message,
playlistId: _variables.query.id,
serverId: _variables.apiClientProps.serverId,
},
});
if (context) {
restorePlaylistQueryData(queryClient, context);
}
@@ -4,7 +4,6 @@ import { AxiosError } from 'axios';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { MutationOptions } from '/@/renderer/lib/react-query';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { RemoveFromPlaylistArgs, RemoveFromPlaylistResponse } from '/@/shared/types/domain-types';
export const useRemoveFromPlaylist = (options?: MutationOptions) => {
@@ -17,17 +16,6 @@ export const useRemoveFromPlaylist = (options?: MutationOptions) => {
apiClientProps: { serverId: args.apiClientProps.serverId },
});
},
onError: (error, variables) => {
logFn.error('Remove from playlist failed', {
category: LogCategory.API,
meta: {
message: error?.message,
playlistId: variables.query.id,
serverId: variables.apiClientProps.serverId,
},
});
options?.onError?.(error);
},
onSuccess: (_data, variables) => {
const { apiClientProps } = variables;
const serverId = apiClientProps.serverId;
@@ -6,7 +6,6 @@ import { queryKeys } from '/@/renderer/api/query-keys';
import { useRecentPlaylists } from '/@/renderer/features/playlists/hooks/use-recent-playlists';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { useCurrentServerId } from '/@/renderer/store';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { ReplacePlaylistArgs, ReplacePlaylistResponse } from '/@/shared/types/domain-types';
export const useReplacePlaylist = (args: MutationHookArgs) => {
@@ -23,17 +22,6 @@ export const useReplacePlaylist = (args: MutationHookArgs) => {
apiClientProps: { serverId: args.apiClientProps.serverId },
});
},
onError: (error, variables) => {
logFn.error('Replace playlist failed', {
category: LogCategory.API,
meta: {
message: error?.message,
playlistId: variables.query.id,
serverId: variables.apiClientProps.serverId,
},
});
options?.onError?.(error);
},
onSuccess: (_data, variables, context) => {
const { apiClientProps } = variables;
const serverId = apiClientProps.serverId;
@@ -4,7 +4,6 @@ import { AxiosError } from 'axios';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { UpdatePlaylistArgs, UpdatePlaylistResponse } from '/@/shared/types/domain-types';
export const useUpdatePlaylist = (args: MutationHookArgs) => {
@@ -18,17 +17,6 @@ export const useUpdatePlaylist = (args: MutationHookArgs) => {
apiClientProps: { serverId: args.apiClientProps.serverId },
});
},
onError: (error, variables) => {
logFn.error('Update playlist failed', {
category: LogCategory.API,
meta: {
message: error?.message,
playlistId: variables.query?.id,
serverId: variables.apiClientProps.serverId,
},
});
options?.onError?.(error);
},
onSuccess: (_data, variables) => {
const { apiClientProps, query } = variables;
const serverId = apiClientProps.serverId;
@@ -4,9 +4,8 @@ import { Suspense, useCallback, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath, useLocation, useNavigate, useParams } from 'react-router';
import { ListContext, useListContext } from '/@/renderer/context/list-context';
import { ListContext } from '/@/renderer/context/list-context';
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { ClientSideSongFilters } from '/@/renderer/features/playlists/components/client-side-song-filters';
import { PlaylistDetailSongListContent } from '/@/renderer/features/playlists/components/playlist-detail-song-list-content';
import { PlaylistDetailSongListHeader } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header';
import {
@@ -14,27 +13,18 @@ import {
PlaylistQueryBuilderRef,
} from '/@/renderer/features/playlists/components/playlist-query-builder';
import { SaveAsPlaylistForm } from '/@/renderer/features/playlists/components/save-as-playlist-form';
import { usePlaylistSongListFilters } from '/@/renderer/features/playlists/hooks/use-playlist-song-list-filters';
import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/create-playlist-mutation';
import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
import { convertQueryGroupToNDQuery } from '/@/renderer/features/playlists/utils';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { JsonPreview } from '/@/renderer/features/shared/components/json-preview';
import { ListWithSidebarContainer } from '/@/renderer/features/shared/components/list-with-sidebar-container';
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
import { AppRoute } from '/@/renderer/router/routes';
import {
PlaylistTarget,
useCurrentServer,
usePageSidebar,
usePlaylistTarget,
} from '/@/renderer/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { PlaylistTarget, useCurrentServer, usePlaylistTarget } from '/@/renderer/store';
import { Button } from '/@/shared/components/button/button';
import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
import { ConfirmModal } from '/@/shared/components/modal/modal';
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
import { Spinner } from '/@/shared/components/spinner/spinner';
import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text';
@@ -246,38 +236,6 @@ const PlaylistQueryEditor = ({
);
};
const PlaylistSongListFiltersSidebar = () => {
const { t } = useTranslation();
const { setIsSidebarOpen } = useListContext();
const { clear } = usePlaylistSongListFilters();
return (
<Stack h="100%" style={{ minHeight: 0 }}>
<Group justify="space-between" pb={0} pl="md" pr="md" pt="md">
<Text fw={500} size="xl">
{t('common.filters', { postProcess: 'sentenceCase' })}
</Text>
<Group gap="xs">
<Button onClick={clear} size="compact-sm" variant="subtle">
{t('common.reset', { postProcess: 'sentenceCase' })}
</Button>
{setIsSidebarOpen && (
<ActionIcon
icon="unpin"
onClick={() => setIsSidebarOpen(false)}
size="compact-sm"
variant="subtle"
/>
)}
</Group>
</Group>
<ScrollArea style={{ flex: 1, minHeight: 0 }}>
<ClientSideSongFilters />
</ScrollArea>
</Stack>
);
};
const PlaylistDetailSongListRoute = () => {
const { t } = useTranslation();
const navigate = useNavigate();
@@ -450,36 +408,23 @@ const PlaylistDetailSongListRoute = () => {
const [itemCount, setItemCount] = useState<number | undefined>(undefined);
const [listData, setListData] = useState<unknown[]>([]);
const [mode, setMode] = useState<'edit' | 'view'>('view');
const [isSidebarOpen, setIsSidebarOpen] = usePageSidebar(listKey);
const providerValue = useMemo(() => {
return {
customFilters: undefined,
displayMode,
id: playlistId,
isSidebarOpen,
isSmartPlaylist,
itemCount,
listData,
listKey,
mode,
pageKey: listKey,
setIsSidebarOpen,
setItemCount,
setListData,
setMode,
};
}, [
playlistId,
isSmartPlaylist,
displayMode,
listKey,
isSidebarOpen,
itemCount,
listData,
mode,
setIsSidebarOpen,
]);
}, [playlistId, isSmartPlaylist, displayMode, listKey, itemCount, listData, mode]);
return (
<AnimatedPage key={`playlist-detail-songList-${playlistId}`}>
@@ -496,14 +441,9 @@ const PlaylistDetailSongListRoute = () => {
onToggleQueryBuilder={handleToggleShowQueryBuilder}
/>
<ListWithSidebarContainer>
<ListWithSidebarContainer.SidebarPortal>
<PlaylistSongListFiltersSidebar />
</ListWithSidebarContainer.SidebarPortal>
<Suspense fallback={<Spinner container />}>
<PlaylistDetailSongListContent />
</Suspense>
</ListWithSidebarContainer>
<Suspense fallback={<Spinner container />}>
<PlaylistDetailSongListContent />
</Suspense>
{(isSmartPlaylist || showQueryBuilder) && (
<PlaylistQueryEditor
createPlaylistMutation={createPlaylistMutation}
@@ -4,7 +4,8 @@ import { useTranslation } from 'react-i18next';
import { useUpdateRadioStation } from '/@/renderer/features/radio/mutations/update-radio-station-mutation';
import { useCurrentServer } from '/@/renderer/store';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { logFn } from '/@/renderer/utils/logger';
import { logMsg } from '/@/renderer/utils/logger-message';
import { Group } from '/@/shared/components/group/group';
import { closeAllModals, openModal } from '/@/shared/components/modal/modal';
import { ModalButton } from '/@/shared/components/modal/model-shared';
@@ -47,8 +48,7 @@ export const EditRadioStationForm = ({ onCancel, station }: EditRadioStationForm
},
{
onError: (error) => {
logFn.error('An error occurred', {
category: LogCategory.OTHER,
logFn.error(logMsg.other.error, {
meta: { error: error as Error },
});
@@ -4,7 +4,6 @@ import { AxiosError } from 'axios';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import {
CreateInternetRadioStationArgs,
CreateInternetRadioStationResponse,
@@ -26,16 +25,6 @@ export const useCreateRadioStation = (args: MutationHookArgs) => {
apiClientProps: { serverId: args.apiClientProps.serverId },
});
},
onError: (error, variables) => {
logFn.error('Create radio station failed', {
category: LogCategory.API,
meta: {
message: error?.message,
serverId: variables.apiClientProps.serverId,
},
});
options?.onError?.(error);
},
onSuccess: (_args, variables) => {
queryClient.invalidateQueries({
exact: false,
@@ -4,7 +4,6 @@ import { AxiosError } from 'axios';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import {
DeleteInternetRadioStationArgs,
DeleteInternetRadioStationResponse,
@@ -26,17 +25,6 @@ export const useDeleteRadioStation = (args: MutationHookArgs) => {
apiClientProps: { serverId: args.apiClientProps.serverId },
});
},
onError: (error, variables) => {
logFn.error('Delete radio station failed', {
category: LogCategory.API,
meta: {
message: error?.message,
serverId: variables.apiClientProps.serverId,
stationId: variables.query?.id,
},
});
options?.onError?.(error);
},
onSuccess: (_args, variables) => {
queryClient.invalidateQueries({
exact: false,
@@ -4,7 +4,6 @@ import { AxiosError } from 'axios';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import {
UpdateInternetRadioStationArgs,
UpdateInternetRadioStationResponse,
@@ -26,17 +25,6 @@ export const useUpdateRadioStation = (args: MutationHookArgs) => {
apiClientProps: { serverId: args.apiClientProps.serverId },
});
},
onError: (error, variables) => {
logFn.error('Update radio station failed', {
category: LogCategory.API,
meta: {
message: error?.message,
serverId: variables.apiClientProps.serverId,
stationId: variables.query?.id,
},
});
options?.onError?.(error);
},
onSuccess: (_args, variables) => {
queryClient.invalidateQueries({
exact: false,
@@ -8,6 +8,7 @@ import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
import { usePlayerActions, usePlayerStore, useRemoteSettings } from '/@/renderer/store';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { logMsg } from '/@/renderer/utils/logger-message';
import { toast } from '/@/shared/components/toast/toast';
import { LibraryItem } from '/@/shared/types/domain-types';
import { PlayerShuffle } from '/@/shared/types/types';
@@ -32,7 +33,7 @@ export const useRemote = () => {
return;
}
logFn.debug('Initializing remote settings', {
logFn.debug(logMsg[LogCategory.REMOTE].initializingRemoteSettings, {
category: LogCategory.REMOTE,
meta: {
enabled: remoteSettings.enabled,
@@ -49,7 +50,7 @@ export const useRemote = () => {
remoteSettings.password,
)
.catch((error) => {
logFn.error('Failed to enable remote', {
logFn.error(logMsg[LogCategory.REMOTE].failedToEnableRemote, {
category: LogCategory.REMOTE,
meta: { error },
});
@@ -65,7 +66,7 @@ export const useRemote = () => {
}
remote.requestPosition((_e: unknown, data: { position: number }) => {
logFn.debug('Request position received', {
logFn.debug(logMsg[LogCategory.REMOTE].requestPositionReceived, {
category: LogCategory.REMOTE,
meta: { position: data.position },
});
@@ -74,7 +75,7 @@ export const useRemote = () => {
});
remote.requestSeek((_e: unknown, data: { offset: number }) => {
logFn.debug('Request seek received', {
logFn.debug(logMsg[LogCategory.REMOTE].requestSeekReceived, {
category: LogCategory.REMOTE,
meta: { offset: data.offset },
});
@@ -83,7 +84,7 @@ export const useRemote = () => {
remote.requestRating(
(_e: unknown, data: { id: string; rating: number; serverId: string }) => {
logFn.debug('Request rating received', {
logFn.debug(logMsg[LogCategory.REMOTE].requestRatingReceived, {
category: LogCategory.REMOTE,
meta: { id: data.id, rating: data.rating, serverId: data.serverId },
});
@@ -92,7 +93,7 @@ export const useRemote = () => {
);
remote.requestVolume((_e: unknown, data: { volume: number }) => {
logFn.debug('Request volume received', {
logFn.debug(logMsg[LogCategory.REMOTE].requestVolumeReceived, {
category: LogCategory.REMOTE,
meta: { volume: data.volume },
});
@@ -101,7 +102,7 @@ export const useRemote = () => {
remote.requestFavorite(
(_e: unknown, data: { favorite: boolean; id: string; serverId: string }) => {
logFn.debug('Request favorite received', {
logFn.debug(logMsg[LogCategory.REMOTE].requestFavoriteReceived, {
category: LogCategory.REMOTE,
meta: { favorite: data.favorite, id: data.id, serverId: data.serverId },
});
@@ -147,7 +148,7 @@ export const useRemote = () => {
const currentSong = player.getCurrentSong();
if (currentSong) {
logFn.debug('Sending initial song', {
logFn.debug(logMsg[LogCategory.REMOTE].sendingInitialSong, {
category: LogCategory.REMOTE,
meta: {
artistName: currentSong.artistName,
@@ -177,7 +178,7 @@ export const useRemote = () => {
return;
}
logFn.debug('Update song sent', {
logFn.debug(logMsg[LogCategory.REMOTE].updateSongSent, {
category: LogCategory.REMOTE,
meta: {
artistName: properties.song?.artistName,
@@ -208,7 +209,7 @@ export const useRemote = () => {
return;
}
logFn.debug('Update position sent', {
logFn.debug(logMsg[LogCategory.REMOTE].updatePositionSent, {
category: LogCategory.REMOTE,
meta: { timestamp: properties.timestamp },
});
@@ -219,7 +220,7 @@ export const useRemote = () => {
return;
}
logFn.debug('Update repeat sent', {
logFn.debug(logMsg[LogCategory.REMOTE].updateRepeatSent, {
category: LogCategory.REMOTE,
meta: { repeat: properties.repeat },
});
@@ -231,7 +232,7 @@ export const useRemote = () => {
}
const isShuffleEnabled = properties.shuffle !== PlayerShuffle.NONE;
logFn.debug('Update shuffle sent', {
logFn.debug(logMsg[LogCategory.REMOTE].updateShuffleSent, {
category: LogCategory.REMOTE,
meta: { isShuffleEnabled, shuffle: properties.shuffle },
});
@@ -242,7 +243,7 @@ export const useRemote = () => {
return;
}
logFn.debug('Update playback sent', {
logFn.debug(logMsg[LogCategory.REMOTE].updatePlaybackSent, {
category: LogCategory.REMOTE,
meta: { status: properties.status },
});
@@ -253,7 +254,7 @@ export const useRemote = () => {
return;
}
logFn.debug('Update volume sent', {
logFn.debug(logMsg[LogCategory.REMOTE].updateVolumeSent, {
category: LogCategory.REMOTE,
meta: { volume: properties.volume },
});
@@ -264,7 +265,7 @@ export const useRemote = () => {
return;
}
logFn.debug('Update favorite sent', {
logFn.debug(logMsg[LogCategory.REMOTE].updateFavoriteSent, {
category: LogCategory.REMOTE,
meta: {
favorite: properties.favorite,
@@ -279,7 +280,7 @@ export const useRemote = () => {
return;
}
logFn.debug('Update rating sent', {
logFn.debug(logMsg[LogCategory.REMOTE].updateRatingSent, {
category: LogCategory.REMOTE,
meta: {
id: properties.id,
@@ -14,7 +14,6 @@ import NavidromeIcon from '/@/renderer/features/servers/assets/navidrome.png';
import SubsonicIcon from '/@/renderer/features/servers/assets/opensubsonic.png';
import { IgnoreCorsSslSwitches } from '/@/renderer/features/servers/components/ignore-cors-ssl-switches';
import { useAuthStoreActions } from '/@/renderer/store';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group';
@@ -150,10 +149,6 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
);
if (!data) {
logFn.error('Add server failed (no data returned)', {
category: LogCategory.SYSTEM,
meta: { name: values.name, serverType: values.type, url: values.url },
});
return toast.error({
message: t('error.authenticationFailed', { postProcess: 'sentenceCase' }),
});
@@ -194,15 +189,6 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
setCurrentServer(serverItem);
closeAllModals();
logFn.info('Add server successful', {
category: LogCategory.SYSTEM,
meta: {
name: values.name,
serverId: serverItem.id,
serverType: values.type,
url: values.url,
},
});
toast.success({
message: t('form.addServer.success', { postProcess: 'sentenceCase' }),
});
@@ -219,15 +205,6 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
}
}
} catch (err: any) {
logFn.error('Add server failed', {
category: LogCategory.SYSTEM,
meta: {
message: err?.message,
name: values.name,
serverType: values.type,
url: values.url,
},
});
setIsLoading(false);
return toast.error({ message: err?.message });
}
@@ -1,7 +1,6 @@
import { ErrorBoundary } from 'react-error-boundary';
import { useTranslation } from 'react-i18next';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { Box } from '/@/shared/components/box/box';
import { Button } from '/@/shared/components/button/button';
import { Center } from '/@/shared/components/center/center';
@@ -44,22 +43,5 @@ interface ComponentErrorBoundaryProps {
}
export const ComponentErrorBoundary = ({ children }: ComponentErrorBoundaryProps) => {
return (
<ErrorBoundary
FallbackComponent={ComponentErrorFallback}
onError={(error, errorInfo) => {
logFn.error('Component error boundary caught an error', {
category: LogCategory.OTHER,
meta: {
componentStack: errorInfo?.componentStack,
message: error?.message,
name: error?.name,
stack: error?.stack,
},
});
}}
>
{children}
</ErrorBoundary>
);
return <ErrorBoundary FallbackComponent={ComponentErrorFallback}>{children}</ErrorBoundary>;
};
@@ -2,7 +2,6 @@ import { ErrorBoundary } from 'react-error-boundary';
import { useTranslation } from 'react-i18next';
import { ServerSelector } from '/@/renderer/features/sidebar/components/server-selector';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { Box } from '/@/shared/components/box/box';
import { Button } from '/@/shared/components/button/button';
import { Center } from '/@/shared/components/center/center';
@@ -86,15 +85,9 @@ export const PageErrorBoundary = ({ children }: PageErrorBoundaryProps) => {
<ErrorBoundary
FallbackComponent={PageErrorFallback}
onError={(error, errorInfo) => {
logFn.error('Page error boundary caught an error', {
category: LogCategory.OTHER,
meta: {
componentStack: errorInfo?.componentStack,
message: error?.message,
name: error?.name,
stack: error?.stack,
},
});
if (process.env.NODE_ENV === 'development') {
console.error('Page error boundary caught an error:', error, errorInfo);
}
}}
onReset={() => {}}
>
@@ -2,7 +2,6 @@ import { ErrorBoundary } from 'react-error-boundary';
import { useTranslation } from 'react-i18next';
import { ServerSelector } from '/@/renderer/features/sidebar/components/server-selector';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { Box } from '/@/shared/components/box/box';
import { Button } from '/@/shared/components/button/button';
import { Center } from '/@/shared/components/center/center';
@@ -92,15 +91,9 @@ export const RouterErrorBoundary = ({ children }: RouterErrorBoundaryProps) => {
<ErrorBoundary
FallbackComponent={RouterErrorFallback}
onError={(error, errorInfo) => {
logFn.error('Router error boundary caught an error', {
category: LogCategory.OTHER,
meta: {
componentStack: errorInfo?.componentStack,
message: error?.message,
name: error?.name,
stack: error?.stack,
},
});
if (process.env.NODE_ENV === 'development') {
console.error('Root error boundary caught an error:', error, errorInfo);
}
}}
onReset={() => {}}
>
@@ -12,7 +12,6 @@ import {
restoreFavoriteQueryData,
} from '/@/renderer/features/shared/mutations/favorite-optimistic-updates';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { toast } from '/@/shared/components/toast/toast';
import { FavoriteArgs, FavoriteResponse, LibraryItem } from '/@/shared/types/domain-types';
@@ -34,15 +33,6 @@ export const useCreateFavorite = (args: MutationHookArgs) => {
},
mutationKey: createFavoriteMutationKey,
onError: (_error, variables, context) => {
logFn.error('Create favorite failed', {
category: LogCategory.API,
meta: {
id: variables.query.id,
message: _error?.message,
serverId: variables.apiClientProps.serverId,
type: variables.query.type,
},
});
if (context) {
restoreFavoriteQueryData(queryClient, context);
}
@@ -12,7 +12,6 @@ import {
restoreFavoriteQueryData,
} from '/@/renderer/features/shared/mutations/favorite-optimistic-updates';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { toast } from '/@/shared/components/toast/toast';
import { FavoriteArgs, FavoriteResponse, LibraryItem } from '/@/shared/types/domain-types';
@@ -34,15 +33,6 @@ export const useDeleteFavorite = (args: MutationHookArgs) => {
},
mutationKey: deleteFavoriteMutationKey,
onError: (_error, _variables, context) => {
logFn.error('Delete favorite failed', {
category: LogCategory.API,
meta: {
id: _variables.query.id,
message: _error?.message,
serverId: _variables.apiClientProps.serverId,
type: _variables.query.type,
},
});
if (context) {
restoreFavoriteQueryData(queryClient, context);
}
@@ -11,7 +11,6 @@ import {
restoreRatingQueryData,
} from '/@/renderer/features/shared/mutations/rating-optimistic-updates';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { toast } from '/@/shared/components/toast/toast';
import { LibraryItem, RatingResponse, SetRatingArgs } from '/@/shared/types/domain-types';
@@ -31,16 +30,6 @@ export const useSetRatingMutation = (args: MutationHookArgs) => {
},
mutationKey: setRatingMutationKey,
onError: (_error, _variables, context) => {
logFn.error('Set rating failed', {
category: LogCategory.API,
meta: {
id: _variables.query.id,
message: _error?.message,
rating: _variables.query.rating,
serverId: _variables.apiClientProps.serverId,
type: _variables.query.type,
},
});
if (context) {
restoreRatingQueryData(queryClient, context);
}
+8 -21
View File
@@ -61,14 +61,10 @@ enum SharedFilterKeys {
enum SongFilterKeys {
_CUSTOM = '_custom',
ALBUM_ARTIST_IDS = 'albumArtistIds',
ALBUM_ARTIST_IDS_MODE = 'albumArtistIdsMode',
ALBUM_IDS = 'albumIds',
ARTIST_IDS = 'artistIds',
ARTIST_IDS_MODE = 'artistIdsMode',
FAVORITE = 'favorite',
GENRE_ID = 'genreIds',
GENRE_ID_MODE = 'genreIdsMode',
HAS_RATING = 'hasRating',
MAX_YEAR = 'maxYear',
MIN_YEAR = 'minYear',
}
@@ -128,23 +124,12 @@ export const createFuseForLibraryItem = <T extends FuseSearchableItem>(
});
}
const sampleItem = items[0];
const stringKeys = Object.keys(sampleItem).filter(
(key) =>
typeof sampleItem[key as keyof T] === 'string' &&
!key.startsWith('_') &&
key !== 'id' &&
key !== 'albumId' &&
key !== 'streamUrl' &&
key !== 'serverId' &&
key !== 'ownerId',
) as string[];
const stringKeys: string[] = [];
const nestedKeys: Array<{ getFn: (item: T) => string; name: string }> = [];
switch (itemType) {
case LibraryItem.ALBUM: {
stringKeys.push('name', 'releaseType');
nestedKeys.push(
{
getFn: (item) => {
@@ -172,6 +157,7 @@ export const createFuseForLibraryItem = <T extends FuseSearchableItem>(
}
case LibraryItem.ALBUM_ARTIST: {
stringKeys.push('name');
nestedKeys.push({
getFn: (item) => {
const aa = item as AlbumArtist;
@@ -185,9 +171,10 @@ export const createFuseForLibraryItem = <T extends FuseSearchableItem>(
case LibraryItem.ARTIST:
case LibraryItem.GENRE:
case LibraryItem.RADIO_STATION:
stringKeys.push('name');
break;
case LibraryItem.PLAYLIST: {
stringKeys.push('name');
nestedKeys.push({
getFn: (item) => {
const p = item as Playlist;
@@ -200,7 +187,8 @@ export const createFuseForLibraryItem = <T extends FuseSearchableItem>(
case LibraryItem.PLAYLIST_SONG:
case LibraryItem.QUEUE_SONG:
case LibraryItem.SONG: {
case LibraryItem.SONG:
stringKeys.push('album', 'name');
nestedKeys.push(
{
getFn: (item) => {
@@ -218,7 +206,6 @@ export const createFuseForLibraryItem = <T extends FuseSearchableItem>(
},
);
break;
}
}
return new Fuse(items, {
@@ -3,7 +3,6 @@ import { AxiosError } from 'axios';
import { api } from '/@/renderer/api';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { AnyLibraryItems, ShareItemArgs, ShareItemResponse } from '/@/shared/types/domain-types';
export const useShareItem = (args: MutationHookArgs) => {
@@ -21,17 +20,6 @@ export const useShareItem = (args: MutationHookArgs) => {
apiClientProps: { serverId: args.apiClientProps.serverId },
});
},
onError: (error, variables) => {
logFn.error('Share item failed', {
category: LogCategory.API,
meta: {
itemType: variables.body?.resourceType,
message: error?.message,
serverId: variables.apiClientProps.serverId,
},
});
options?.onError?.(error);
},
retry: false,
...options,
});
@@ -52,6 +52,7 @@ export const SongListHeaderFilters = ({ toggleGenreTarget }: { toggleGenreTarget
const query = songFilters.query;
return Boolean(
isFilterValueSet(query[FILTER_KEYS.SONG._CUSTOM]) ||
isFilterValueSet(query[FILTER_KEYS.SONG.ALBUM_IDS]) ||
isFilterValueSet(query[FILTER_KEYS.SONG.ARTIST_IDS]) ||
query[FILTER_KEYS.SONG.FAVORITE] !== undefined ||
isFilterValueSet(query[FILTER_KEYS.SONG.GENRE_ID]) ||
@@ -28,6 +28,11 @@ export const useSongListFilters = (listKey?: ItemListKey) => {
const [searchParams, setSearchParams] = useSearchParams();
const albumIds = useMemo(
() => parseArrayParam(searchParams, FILTER_KEYS.SONG.ALBUM_IDS),
[searchParams],
);
const genreId = useMemo(
() => parseArrayParam(searchParams, FILTER_KEYS.SONG.GENRE_ID),
[searchParams],
@@ -58,6 +63,15 @@ export const useSongListFilters = (listKey?: ItemListKey) => {
[searchParams],
);
const setAlbumIds = useCallback(
(value: null | string[]) => {
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.ALBUM_IDS, value), {
replace: true,
});
},
[setSearchParams],
);
const setGenreId = useCallback(
(value: null | string[]) => {
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.GENRE_ID, value), {
@@ -139,6 +153,7 @@ export const useSongListFilters = (listKey?: ItemListKey) => {
{
[FILTER_KEYS.SHARED.SEARCH_TERM]: null,
[FILTER_KEYS.SONG._CUSTOM]: null,
[FILTER_KEYS.SONG.ALBUM_IDS]: null,
[FILTER_KEYS.SONG.ARTIST_IDS]: null,
[FILTER_KEYS.SONG.FAVORITE]: null,
[FILTER_KEYS.SONG.GENRE_ID]: null,
@@ -157,18 +172,31 @@ export const useSongListFilters = (listKey?: ItemListKey) => {
[FILTER_KEYS.SHARED.SORT_BY]: sortBy ?? undefined,
[FILTER_KEYS.SHARED.SORT_ORDER]: sortOrder ?? undefined,
[FILTER_KEYS.SONG._CUSTOM]: custom ?? undefined,
[FILTER_KEYS.SONG.ALBUM_IDS]: albumIds ?? undefined,
[FILTER_KEYS.SONG.ARTIST_IDS]: artistIds ?? undefined,
[FILTER_KEYS.SONG.FAVORITE]: favorite ?? undefined,
[FILTER_KEYS.SONG.GENRE_ID]: genreId ?? undefined,
[FILTER_KEYS.SONG.MAX_YEAR]: maxYear ?? undefined,
[FILTER_KEYS.SONG.MIN_YEAR]: minYear ?? undefined,
}),
[searchTerm, sortBy, sortOrder, custom, artistIds, favorite, genreId, maxYear, minYear],
[
searchTerm,
sortBy,
sortOrder,
custom,
albumIds,
artistIds,
favorite,
genreId,
maxYear,
minYear,
],
);
return {
clear,
query,
setAlbumIds,
setArtistIds,
setCustom,
setFavorite,
+1 -13
View File
@@ -2,8 +2,6 @@ import { useQuery } from '@tanstack/react-query';
import isElectron from 'is-electron';
import { useEffect, useState } from 'react';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
const CHECK_FOR_UPDATES_INTERVAL_MS = 6 * 60 * 60 * 1000;
const utils = isElectron() ? window.api?.utils : null;
@@ -23,17 +21,7 @@ export const useCheckForUpdates = () => {
return useQuery({
enabled: isEnabled,
queryFn: async () => {
const result = await utils?.checkForUpdates?.();
logFn.info('Check for updates completed', {
category: LogCategory.SYSTEM,
meta: {
updateAvailable: result?.updateAvailable ?? false,
version: result?.version,
},
});
return result;
},
queryFn: () => utils?.checkForUpdates?.(),
queryKey: ['app-check-for-updates'],
refetchInterval: CHECK_FOR_UPDATES_INTERVAL_MS,
refetchIntervalInBackground: true,
+13 -21
View File
@@ -10,6 +10,7 @@ import { controller } from '/@/renderer/api/controller';
import { AppRoute } from '/@/renderer/router/routes';
import { getServerById, useAuthStoreActions, useCurrentServer } from '/@/renderer/store';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { logMsg } from '/@/renderer/utils/logger-message';
import { toast } from '/@/shared/components/toast/toast';
import { AuthState } from '/@/shared/types/types';
@@ -61,7 +62,7 @@ export const useServerAuthenticated = () => {
}
// First, try getUserInfo to check if current credentials are still valid
logFn.info('Authenticating server', {
logFn.info(logMsg[LogCategory.SYSTEM].authenticatingServer, {
category: LogCategory.SYSTEM,
meta: {
method: 'getUserInfo',
@@ -116,7 +117,7 @@ export const useServerAuthenticated = () => {
}
} catch (serverInfoError) {
// Log but don't fail authentication if server info fetch fails
logFn.warn('Server authentication successful', {
logFn.warn(logMsg[LogCategory.SYSTEM].serverAuthenticationSuccess, {
category: LogCategory.SYSTEM,
meta: {
action: 'server_info_fetch_failed',
@@ -127,7 +128,7 @@ export const useServerAuthenticated = () => {
});
}
logFn.info('Server authentication successful', {
logFn.info(logMsg[LogCategory.SYSTEM].serverAuthenticationSuccess, {
category: LogCategory.SYSTEM,
meta: {
isAdmin: userInfo.isAdmin,
@@ -161,7 +162,7 @@ export const useServerAuthenticated = () => {
const password = await localSettings.passwordGet(serverWithAuth.id);
if (password) {
logFn.info('Authenticating server', {
logFn.info(logMsg[LogCategory.SYSTEM].authenticatingServer, {
category: LogCategory.SYSTEM,
meta: {
method: 'authenticate',
@@ -226,7 +227,7 @@ export const useServerAuthenticated = () => {
}
} catch (serverInfoError) {
// Log but don't fail authentication if server info fetch fails
logFn.warn('Server authentication successful', {
logFn.warn(logMsg[LogCategory.SYSTEM].serverAuthenticationSuccess, {
category: LogCategory.SYSTEM,
meta: {
action: 'server_info_fetch_failed',
@@ -237,7 +238,7 @@ export const useServerAuthenticated = () => {
});
}
logFn.info('Server authentication successful', {
logFn.info(logMsg[LogCategory.SYSTEM].serverAuthenticationSuccess, {
category: LogCategory.SYSTEM,
meta: {
isAdmin: authData.isAdmin,
@@ -274,7 +275,7 @@ export const useServerAuthenticated = () => {
if (isNetwork && retryAttempt < MAX_NETWORK_RETRIES) {
const nextRetry = retryAttempt + 1;
logFn.warn('Server authentication failed', {
logFn.warn(logMsg[LogCategory.SYSTEM].serverAuthenticationFailed, {
category: LogCategory.SYSTEM,
meta: {
action: 'network_error_retry',
@@ -297,7 +298,7 @@ export const useServerAuthenticated = () => {
// If network error and retries exhausted, redirect to no-network page
if (isNetwork && retryAttempt >= MAX_NETWORK_RETRIES) {
logFn.error('Server authentication failed', {
logFn.error(logMsg[LogCategory.SYSTEM].serverAuthenticationFailed, {
category: LogCategory.SYSTEM,
meta: {
action: 'network_error_max_retries_exceeded',
@@ -316,7 +317,7 @@ export const useServerAuthenticated = () => {
}
// For non-network errors, handle normally
logFn.error('Server authentication failed', {
logFn.error(logMsg[LogCategory.SYSTEM].serverAuthenticationFailed, {
category: LogCategory.SYSTEM,
meta: {
error: errorMessage,
@@ -345,23 +346,14 @@ export const useServerAuthenticated = () => {
const debouncedAuth = debounce(
(serverWithAuth: NonNullable<ReturnType<typeof getServerById>>) => {
authenticateServer(serverWithAuth).catch((err) => {
logFn.error('Server authentication failed (debounced)', {
category: LogCategory.SYSTEM,
meta: {
message: (err as Error)?.message,
serverId: serverWithAuth.id,
serverName: serverWithAuth.name,
},
});
});
authenticateServer(serverWithAuth).catch(console.error);
},
300,
);
useEffect(() => {
if (!server) {
logFn.debug('Server authentication invalid', {
logFn.debug(logMsg[LogCategory.SYSTEM].serverAuthenticationInvalid, {
category: LogCategory.SYSTEM,
meta: {
reason: 'No server selected',
@@ -377,7 +369,7 @@ export const useServerAuthenticated = () => {
retryCountRef.current = 0; // Reset retry count when server changes
if (!serverWithAuth) {
logFn.error('Server authentication error', {
logFn.error(logMsg[LogCategory.SYSTEM].serverAuthenticationError, {
category: LogCategory.SYSTEM,
meta: {
reason: 'Server not found in store',
@@ -4,7 +4,8 @@ import { useEffect, useRef } from 'react';
import i18n from '/@/i18n/i18n';
import { openRestartRequiredToast } from '/@/renderer/features/settings/restart-toast';
import { useSettingsStore } from '/@/renderer/store/settings.store';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { logFn } from '/@/renderer/utils/logger';
import { logMsg } from '/@/renderer/utils/logger-message';
// Synchronizes settings from the renderer store to the main process electron store
// on app initialization. If there are differences, it updates the main store and shows
@@ -119,8 +120,7 @@ export const useSyncSettingsToMain = () => {
JSON.stringify(rendererValueNormalized)
) {
hasDifferences = true;
logFn.warn('Differences found between renderer and main process settings', {
category: LogCategory.SYSTEM,
logFn.warn(logMsg.system.settingsSynchronized, {
meta: {
mainStoreKey: mapping.mainStoreKey,
mainValue: mainValueNormalized,
+1 -8
View File
@@ -8,19 +8,12 @@ import type {
import { QueryCache, QueryClient } from '@tanstack/react-query';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { toast } from '/@/shared/components/toast/toast';
const queryCache = new QueryCache({
onError: (error: any, query) => {
logFn.error('Query failed', {
category: LogCategory.API,
meta: {
message: error?.message,
queryKey: query.queryKey,
},
});
if (query.state.data !== undefined) {
console.error(error);
toast.show({ message: `${error.message}`, type: 'error' });
}
},
+4 -17
View File
@@ -8,7 +8,6 @@ import { useTranslation } from 'react-i18next';
import packageJson from '../../package.json';
import { formatHrDateTime } from '/@/renderer/utils/format';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { Button } from '/@/shared/components/button/button';
import { Center } from '/@/shared/components/center/center';
import { Group } from '/@/shared/components/group/group';
@@ -71,22 +70,10 @@ const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) =
// Fetch list of recent releases for the selector
const { data: releasesList = [] } = useQuery({
queryFn: async () => {
try {
const response = await axios.get<GitHubRelease[]>(GITHUB_RELEASES_URL, {
params: { per_page: RELEASES_TO_FETCH },
});
logFn.info('Release notes fetched', {
category: LogCategory.GENERAL,
meta: { count: response.data?.length ?? 0 },
});
return response.data;
} catch (error) {
logFn.error('Release notes fetch failed', {
category: LogCategory.GENERAL,
meta: { message: (error as Error)?.message },
});
throw error;
}
const response = await axios.get<GitHubRelease[]>(GITHUB_RELEASES_URL, {
params: { per_page: RELEASES_TO_FETCH },
});
return response.data;
},
queryKey: ['github-releases-list'],
retry: 2,
-32
View File
@@ -10,12 +10,8 @@ export interface AppSlice extends AppState {
actions: {
setAlbumArtistDetailGroupingType: (groupingType: 'all' | 'primary') => void;
setAlbumArtistDetailSort: (sortBy: AlbumListSort, sortOrder: SortOrder) => void;
setAlbumArtistIdsMode: (mode: 'and' | 'or') => void;
setAlbumArtistSelectMode: (mode: 'multi' | 'single') => void;
setAppStore: (data: Partial<AppSlice>) => void;
setArtistIdsMode: (mode: 'and' | 'or') => void;
setArtistSelectMode: (mode: 'multi' | 'single') => void;
setGenreIdsMode: (mode: 'and' | 'or') => void;
setGenreSelectMode: (mode: 'multi' | 'single') => void;
setPageSidebar: (key: string, value: boolean) => void;
setPrivateMode: (enabled: boolean) => void;
@@ -31,12 +27,8 @@ export interface AppState {
sortBy: AlbumListSort;
sortOrder: SortOrder;
};
albumArtistIdsMode: 'and' | 'or';
albumArtistSelectMode: 'multi' | 'single';
artistIdsMode: 'and' | 'or';
artistSelectMode: 'multi' | 'single';
commandPalette: CommandPaletteProps;
genreIdsMode: 'and' | 'or';
genreSelectMode: 'multi' | 'single';
isReorderingQueue: boolean;
pageSidebar: Record<string, boolean>;
@@ -87,34 +79,14 @@ export const useAppStore = createWithEqualityFn<AppSlice>()(
};
});
},
setAlbumArtistIdsMode: (mode) => {
set((state) => {
state.albumArtistIdsMode = mode;
});
},
setAlbumArtistSelectMode: (mode) => {
set((state) => {
state.albumArtistSelectMode = mode;
});
},
setAppStore: (data) => {
set({ ...get(), ...data });
},
setArtistIdsMode: (mode) => {
set((state) => {
state.artistIdsMode = mode;
});
},
setArtistSelectMode: (mode) => {
set((state) => {
state.artistSelectMode = mode;
});
},
setGenreIdsMode: (mode) => {
set((state) => {
state.genreIdsMode = mode;
});
},
setGenreSelectMode: (mode) => {
set((state) => {
state.genreSelectMode = mode;
@@ -151,9 +123,6 @@ export const useAppStore = createWithEqualityFn<AppSlice>()(
sortBy: AlbumListSort.RELEASE_DATE,
sortOrder: SortOrder.DESC,
},
albumArtistIdsMode: 'and',
albumArtistSelectMode: 'multi',
artistIdsMode: 'and',
artistSelectMode: 'multi',
commandPalette: {
close: () => {
@@ -173,7 +142,6 @@ export const useAppStore = createWithEqualityFn<AppSlice>()(
});
},
},
genreIdsMode: 'and',
genreSelectMode: 'multi',
isReorderingQueue: false,
pageSidebar: {
-26
View File
@@ -5,7 +5,6 @@ import { immer } from 'zustand/middleware/immer';
import { shallow } from 'zustand/shallow';
import { createWithEqualityFn } from 'zustand/traditional';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { ServerListItem, ServerListItemWithCredential } from '/@/shared/types/domain-types';
export interface AuthSlice extends AuthState {
@@ -31,16 +30,6 @@ export const useAuthStore = createWithEqualityFn<AuthSlice>()(
immer((set, get) => ({
actions: {
addServer: (args) => {
if (process.env.NODE_ENV === 'development') {
logFn.debug('Auth store: add server', {
category: LogCategory.SYSTEM,
meta: {
serverId: args.id,
serverName: args.name,
serverType: args.type,
},
});
}
set((state) => {
state.serverList[args.id] = args;
});
@@ -60,15 +49,6 @@ export const useAuthStore = createWithEqualityFn<AuthSlice>()(
return null;
},
setCurrentServer: (server) => {
if (process.env.NODE_ENV === 'development') {
logFn.debug('Auth store: set current server', {
category: LogCategory.SYSTEM,
meta: {
serverId: server?.id ?? null,
serverName: server?.name ?? null,
},
});
}
set((state) => {
state.currentServer = server;
});
@@ -85,12 +65,6 @@ export const useAuthStore = createWithEqualityFn<AuthSlice>()(
});
},
updateServer: (id: string, args: Partial<ServerListItemWithCredential>) => {
if (process.env.NODE_ENV === 'development') {
logFn.debug('Auth store: update server', {
category: LogCategory.SYSTEM,
meta: { keys: Object.keys(args || {}), serverId: id },
});
}
set((state) => {
const updatedServer = {
...state.serverList[id],
+123
View File
@@ -0,0 +1,123 @@
import { LogCategory } from '/@/renderer/utils/logger';
export const logMsg = {
[LogCategory.ANALYTICS]: {
appTracked: 'Analytics sent',
pageViewTracked: 'Page view tracked',
},
[LogCategory.API]: {},
[LogCategory.EXTERNAL]: {
discordRpcActivityCleared: 'Activity was cleared for Discord RPC',
discordRpcInitialized: 'Discord RPC was initialized',
discordRpcQuit: 'Discord RPC was quit',
discordRpcSetActivity: 'Activity was set for Discord RPC',
discordRpcTrackChanged: 'Track was changed for Discord RPC',
discordRpcUpdateSkipped: 'Activity was not updated for Discord RPC',
},
[LogCategory.OTHER]: {
error: 'An error occurred',
warning: 'A warning occurred',
},
[LogCategory.PLAYER]: {
addToQueueByData: 'Added to queue by data',
addToQueueByFetch: 'Added to queue by fetch',
addToQueueByListQuery: 'Added to queue by list query',
addToQueueByType: 'Added to queue by type',
autoPlayFailed: 'Auto play failed',
autoPlayTriggered: 'Auto play triggered',
cancelledFetch: 'Cancelled fetch',
clearQueue: 'Cleared queue',
clearSelected: 'Cleared selected',
decreaseVolume: 'Decreased volume',
increaseVolume: 'Increased volume',
mediaNext: 'Media next',
mediaPause: 'Media pause',
mediaPlay: 'Media play',
mediaPlayByIndex: 'Media play by index',
mediaPrevious: 'Media previous',
mediaSeekToTimestamp: 'Media seek to timestamp',
mediaSkipBackward: 'Media skip backward',
mediaSkipForward: 'Media skip forward',
mediaStop: 'Media stop',
mediaToggleMute: 'Media toggle mute',
mediaTogglePlayPause: 'Media toggle play pause',
moveSelectedTo: 'Moved selected to',
moveSelectedToBottom: 'Moved selected to bottom',
moveSelectedToNext: 'Moved selected to next',
moveSelectedToTop: 'Moved selected to top',
playbackError: 'An error occurred during playback',
playerFiltersApplied: 'Player filters applied',
setFavorite: 'Set favorite',
setQueue: 'Set queue',
setRating: 'Set rating',
setRepeat: 'Set repeat',
setShuffle: 'Set shuffle',
setSpeed: 'Set speed',
setVolume: 'Set volume',
shuffle: 'Shuffle',
shuffleAll: 'Shuffle all',
shuffleSelected: 'Shuffle selected',
toggleRepeat: 'Toggle repeat',
toggleShuffle: 'Toggle shuffle',
},
[LogCategory.REMOTE]: {
cannotSendEvent: 'Cannot send event - socket not available',
closingExistingSocket: 'Closing existing socket',
creatingWebSocket: 'Creating new WebSocket',
credentialsFetched: 'Credentials fetched',
failedToEnableRemote: 'Failed to enable remote',
failedToGetCredentials: 'Failed to get credentials',
favoriteEventReceived: 'Favorite event received',
fetchingCredentials: 'Fetching credentials',
initializingRemoteSettings: 'Initializing remote settings',
playbackEventReceived: 'Playback event received',
positionEventReceived: 'Position event received',
proxyEventReceived: 'Proxy event received (image update)',
ratingEventReceived: 'Rating event received',
reconnectInitiated: 'Reconnect initiated',
reloadingPage: 'Reloading page due to close code',
repeatEventReceived: 'Repeat event received',
requestFavoriteReceived: 'Request favorite received',
requestPositionReceived: 'Request position received',
requestRatingReceived: 'Request rating received',
requestSeekReceived: 'Request seek received',
requestVolumeReceived: 'Request volume received',
sendingAuthentication: 'Sending authentication',
sendingEventToServer: 'Sending event to server',
sendingInitialSong: 'Sending initial song',
serverIsDown: 'Server is down',
shuffleEventReceived: 'Shuffle event received',
socketClosedUnexpectedly: 'Socket closed unexpectedly',
songEventReceived: 'Song event received',
stateEventReceived: 'State event received (full state update)',
updateFavoriteSent: 'Update favorite sent',
updatePlaybackSent: 'Update playback sent',
updatePositionSent: 'Update position sent',
updateRatingSent: 'Update rating sent',
updateRepeatSent: 'Update repeat sent',
updateShuffleSent: 'Update shuffle sent',
updateSongSent: 'Update song sent',
updateVolumeSent: 'Update volume sent',
volumeEventReceived: 'Volume event received',
webSocketClosed: 'WebSocket closed',
webSocketErrorEvent: 'WebSocket error event',
webSocketMessageReceived: 'WebSocket message received',
webSocketOpened: 'WebSocket opened',
},
[LogCategory.SCROBBLE]: {
scrobbledPause: 'Scrobbled a pause event',
scrobbledStart: 'Scrobbled a start event',
scrobbledSubmission: 'Scrobbled a submission event',
scrobbledTimeupdate: 'Scrobbled a timeupdate event',
scrobbledUnpause: 'Scrobbled an unpause event',
},
[LogCategory.SYSTEM]: {
authenticatingServer: 'Authenticating server',
serverAuthenticationAborted: 'Server authentication aborted',
serverAuthenticationError: 'Server authentication error',
serverAuthenticationFailed: 'Server authentication failed',
serverAuthenticationInvalid: 'Server authentication invalid',
serverAuthenticationSuccess: 'Server authentication successful',
settingsSynchronized: 'Differences found between renderer and main process settings',
},
};