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