mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-22 10:26:33 +02:00
lint fixes
This commit is contained in:
@@ -6,11 +6,7 @@ import type {
|
|||||||
|
|
||||||
import { ipcMain, type IpcMainInvokeEvent } from 'electron';
|
import { ipcMain, type IpcMainInvokeEvent } from 'electron';
|
||||||
|
|
||||||
import {
|
import { readFilesMetadataBatch, readLocalImageFile, writeFilesTags } from './taglib-service';
|
||||||
readFilesMetadataBatch,
|
|
||||||
readLocalImageFile,
|
|
||||||
writeFilesTags,
|
|
||||||
} from './taglib-service';
|
|
||||||
|
|
||||||
const sendBatchProgress = (event: IpcMainInvokeEvent, processed: number, total: number) => {
|
const sendBatchProgress = (event: IpcMainInvokeEvent, processed: number, total: number) => {
|
||||||
event.sender.send('batch-progress', { processed, total });
|
event.sender.send('batch-progress', { processed, total });
|
||||||
@@ -46,8 +42,8 @@ ipcMain.handle(
|
|||||||
artworkKind: 'none',
|
artworkKind: 'none',
|
||||||
error: result.failedFiles[0]?.error ?? 'No readable audio files in selection',
|
error: result.failedFiles[0]?.error ?? 'No readable audio files in selection',
|
||||||
failedFiles: result.failedFiles,
|
failedFiles: result.failedFiles,
|
||||||
success: false,
|
|
||||||
readCount: result.readCount,
|
readCount: result.readCount,
|
||||||
|
success: false,
|
||||||
totalCount: result.totalCount,
|
totalCount: result.totalCount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -55,8 +51,8 @@ ipcMain.handle(
|
|||||||
return {
|
return {
|
||||||
artworkKind: result.artworkKind,
|
artworkKind: result.artworkKind,
|
||||||
failedFiles: result.failedFiles.length > 0 ? result.failedFiles : undefined,
|
failedFiles: result.failedFiles.length > 0 ? result.failedFiles : undefined,
|
||||||
success: true,
|
|
||||||
readCount: result.readCount,
|
readCount: result.readCount,
|
||||||
|
success: true,
|
||||||
tagSummary: result.tagSummary,
|
tagSummary: result.tagSummary,
|
||||||
totalCount: result.totalCount,
|
totalCount: result.totalCount,
|
||||||
...(result.artworkData
|
...(result.artworkData
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import type { ArtworkKind, ArtworkOp, BatchFileError } from '/@/shared/types/tag-editor';
|
import type { ArtworkKind, ArtworkOp, BatchFileError } from '/@/shared/types/tag-editor';
|
||||||
import type { ICommonTagsResult } from 'music-metadata';
|
import type { ICommonTagsResult } from 'music-metadata';
|
||||||
|
|
||||||
import { EDITOR_FIELD_KEYS } from '/@/shared/types/tag-editor';
|
|
||||||
|
|
||||||
import { constants, promises as fsPromises } from 'fs';
|
import { constants, promises as fsPromises } from 'fs';
|
||||||
import * as mm from 'music-metadata';
|
import * as mm from 'music-metadata';
|
||||||
import { TagLib } from 'taglib-wasm';
|
import { TagLib } from 'taglib-wasm';
|
||||||
|
|
||||||
|
import { EDITOR_FIELD_KEYS } from '/@/shared/types/tag-editor';
|
||||||
import { getImageMimeTypeFromPath } from '/@/shared/utils/image-mime';
|
import { getImageMimeTypeFromPath } from '/@/shared/utils/image-mime';
|
||||||
|
|
||||||
let _taglib: null | TagLib = null;
|
let _taglib: null | TagLib = null;
|
||||||
@@ -65,20 +64,6 @@ const MM_CUSTOM: Partial<Record<string, TagAccessor>> = {
|
|||||||
trackNumber: (c) => c.track.no,
|
trackNumber: (c) => c.track.no,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Maps a music-metadata `ICommonTagsResult` to the flat camelCase key map used by the editor. */
|
|
||||||
export function flattenMusicMetadata(common: ICommonTagsResult): Record<string, string> {
|
|
||||||
const flat: Record<string, string> = {};
|
|
||||||
for (const key of EDITOR_FIELD_KEYS) {
|
|
||||||
const custom = MM_CUSTOM[key];
|
|
||||||
const raw: unknown = custom
|
|
||||||
? custom(common)
|
|
||||||
: common[(MM_RENAMES[key] ?? key) as keyof ICommonTagsResult];
|
|
||||||
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
||||||
if (value != null && value !== '') flat[key] = String(value);
|
|
||||||
}
|
|
||||||
return flat;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns an error entry for each path that is missing or not writable by the current process. */
|
/** Returns an error entry for each path that is missing or not writable by the current process. */
|
||||||
export async function checkPathsWritable(paths: string[]): Promise<BatchFileError[]> {
|
export async function checkPathsWritable(paths: string[]): Promise<BatchFileError[]> {
|
||||||
const failed: BatchFileError[] = [];
|
const failed: BatchFileError[] = [];
|
||||||
@@ -97,6 +82,20 @@ export async function checkPathsWritable(paths: string[]): Promise<BatchFileErro
|
|||||||
return failed;
|
return failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Maps a music-metadata `ICommonTagsResult` to the flat camelCase key map used by the editor. */
|
||||||
|
export function flattenMusicMetadata(common: ICommonTagsResult): Record<string, string> {
|
||||||
|
const flat: Record<string, string> = {};
|
||||||
|
for (const key of EDITOR_FIELD_KEYS) {
|
||||||
|
const custom = MM_CUSTOM[key];
|
||||||
|
const raw: unknown = custom
|
||||||
|
? custom(common)
|
||||||
|
: common[(MM_RENAMES[key] ?? key) as keyof ICommonTagsResult];
|
||||||
|
const value = Array.isArray(raw) ? raw[0] : raw;
|
||||||
|
if (value != null && value !== '') flat[key] = String(value);
|
||||||
|
}
|
||||||
|
return flat;
|
||||||
|
}
|
||||||
|
|
||||||
/** Runs `fn` over `items` with at most `concurrency` tasks in flight at once. Stops early if `signal` is aborted. */
|
/** Runs `fn` over `items` with at most `concurrency` tasks in flight at once. Stops early if `signal` is aborted. */
|
||||||
const mapWithConcurrency = async <T>(
|
const mapWithConcurrency = async <T>(
|
||||||
items: T[],
|
items: T[],
|
||||||
@@ -130,8 +129,8 @@ export async function readFilesMetadataBatch(
|
|||||||
artworkKind: ArtworkKind;
|
artworkKind: ArtworkKind;
|
||||||
artworkMimeType?: string;
|
artworkMimeType?: string;
|
||||||
failedFiles: BatchFileError[];
|
failedFiles: BatchFileError[];
|
||||||
success: boolean;
|
|
||||||
readCount: number;
|
readCount: number;
|
||||||
|
success: boolean;
|
||||||
tagSummary: Record<string, null | string>;
|
tagSummary: Record<string, null | string>;
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
}> {
|
}> {
|
||||||
@@ -216,8 +215,8 @@ export async function readFilesMetadataBatch(
|
|||||||
return {
|
return {
|
||||||
artworkKind,
|
artworkKind,
|
||||||
failedFiles,
|
failedFiles,
|
||||||
success: readCount > 0,
|
|
||||||
readCount,
|
readCount,
|
||||||
|
success: readCount > 0,
|
||||||
tagSummary,
|
tagSummary,
|
||||||
totalCount,
|
totalCount,
|
||||||
...(artworkData ? { artworkData, artworkMimeType } : {}),
|
...(artworkData ? { artworkData, artworkMimeType } : {}),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type IpcRendererEvent, ipcRenderer, webFrame } from 'electron';
|
import { ipcRenderer, type IpcRendererEvent, webFrame } from 'electron';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ArtworkOp,
|
ArtworkOp,
|
||||||
|
|||||||
@@ -864,6 +864,18 @@ export const controller: GeneralController = {
|
|||||||
server.type,
|
server.type,
|
||||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||||
},
|
},
|
||||||
|
startScan(args) {
|
||||||
|
const server = getServerById(args.apiClientProps.serverId);
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error(`${i18n.t('error.apiRouteError')}: startScan`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiController(
|
||||||
|
'startScan',
|
||||||
|
server.type,
|
||||||
|
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||||
|
},
|
||||||
updateInternetRadioStation(args) {
|
updateInternetRadioStation(args) {
|
||||||
const server = getServerById(args.apiClientProps.serverId);
|
const server = getServerById(args.apiClientProps.serverId);
|
||||||
|
|
||||||
@@ -924,16 +936,4 @@ export const controller: GeneralController = {
|
|||||||
server.type,
|
server.type,
|
||||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||||
},
|
},
|
||||||
startScan(args) {
|
|
||||||
const server = getServerById(args.apiClientProps.serverId);
|
|
||||||
|
|
||||||
if (!server) {
|
|
||||||
throw new Error(`${i18n.t('error.apiRouteError')}: startScan`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiController(
|
|
||||||
'startScan',
|
|
||||||
server.type,
|
|
||||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -365,6 +365,16 @@ export const contract = c.router({
|
|||||||
400: jfType._response.error,
|
400: jfType._response.error,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
startScan: {
|
||||||
|
body: z.null(),
|
||||||
|
method: 'POST',
|
||||||
|
path: 'Library/Refresh',
|
||||||
|
query: z.object({}),
|
||||||
|
responses: {
|
||||||
|
204: z.null(),
|
||||||
|
400: jfType._response.error,
|
||||||
|
},
|
||||||
|
},
|
||||||
updatePlaylist: {
|
updatePlaylist: {
|
||||||
body: jfType._parameters.updatePlaylist,
|
body: jfType._parameters.updatePlaylist,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -392,16 +402,6 @@ export const contract = c.router({
|
|||||||
400: jfType._response.error,
|
400: jfType._response.error,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
startScan: {
|
|
||||||
body: z.null(),
|
|
||||||
method: 'POST',
|
|
||||||
path: 'Library/Refresh',
|
|
||||||
query: z.object({}),
|
|
||||||
responses: {
|
|
||||||
204: z.null(),
|
|
||||||
400: jfType._response.error,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const axiosClient = axios.create({});
|
const axiosClient = axios.create({});
|
||||||
|
|||||||
@@ -1931,6 +1931,20 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
startScan: async (args) => {
|
||||||
|
const { apiClientProps } = args;
|
||||||
|
|
||||||
|
const res = await jfApiClient(apiClientProps).startScan({
|
||||||
|
body: null,
|
||||||
|
query: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 204) {
|
||||||
|
throw new Error('Failed to start scan');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
updateInternetRadioStation: async (args) => {
|
updateInternetRadioStation: async (args) => {
|
||||||
const { apiClientProps, body, query } = args;
|
const { apiClientProps, body, query } = args;
|
||||||
|
|
||||||
@@ -1996,20 +2010,6 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
'Failed to upload playlist image',
|
'Failed to upload playlist image',
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
startScan: async (args) => {
|
|
||||||
const { apiClientProps } = args;
|
|
||||||
|
|
||||||
const res = await jfApiClient(apiClientProps).startScan({
|
|
||||||
body: null,
|
|
||||||
query: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status !== 204) {
|
|
||||||
throw new Error('Failed to start scan');
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function getLibraryId(musicFolderId?: string | string[]) {
|
function getLibraryId(musicFolderId?: string | string[]) {
|
||||||
|
|||||||
@@ -1261,6 +1261,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
id: res.body.data.id,
|
id: res.body.data.id,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
startScan: SubsonicController.startScan,
|
||||||
updateInternetRadioStation: async (args) => {
|
updateInternetRadioStation: async (args) => {
|
||||||
const { apiClientProps, body, query } = args;
|
const { apiClientProps, body, query } = args;
|
||||||
|
|
||||||
@@ -1411,5 +1412,4 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
|
|
||||||
return res.data?.status === 'ok';
|
return res.data?.status === 'ok';
|
||||||
},
|
},
|
||||||
startScan: SubsonicController.startScan,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -186,6 +186,14 @@ export const contract = c.router({
|
|||||||
200: ssType._response.randomSongList,
|
200: ssType._response.randomSongList,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
getScanStatus: {
|
||||||
|
method: 'GET',
|
||||||
|
path: 'getScanStatus.view',
|
||||||
|
query: ssType._parameters.getScanStatus,
|
||||||
|
responses: {
|
||||||
|
200: ssType._response.getScanStatus,
|
||||||
|
},
|
||||||
|
},
|
||||||
getServerInfo: {
|
getServerInfo: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: 'getOpenSubsonicExtensions.view',
|
path: 'getOpenSubsonicExtensions.view',
|
||||||
@@ -337,6 +345,14 @@ export const contract = c.router({
|
|||||||
200: ssType._response.setRating,
|
200: ssType._response.setRating,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
startScan: {
|
||||||
|
method: 'GET',
|
||||||
|
path: 'startScan.view',
|
||||||
|
query: ssType._parameters.startScan,
|
||||||
|
responses: {
|
||||||
|
200: ssType._response.startScan,
|
||||||
|
},
|
||||||
|
},
|
||||||
updateInternetRadioStation: {
|
updateInternetRadioStation: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: 'updateInternetRadioStation.view',
|
path: 'updateInternetRadioStation.view',
|
||||||
@@ -353,22 +369,6 @@ export const contract = c.router({
|
|||||||
200: ssType._response.baseResponse,
|
200: ssType._response.baseResponse,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
startScan: {
|
|
||||||
method: 'GET',
|
|
||||||
path: 'startScan.view',
|
|
||||||
query: ssType._parameters.startScan,
|
|
||||||
responses: {
|
|
||||||
200: ssType._response.startScan,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
getScanStatus: {
|
|
||||||
method: 'GET',
|
|
||||||
path: 'getScanStatus.view',
|
|
||||||
query: ssType._parameters.getScanStatus,
|
|
||||||
responses: {
|
|
||||||
200: ssType._response.getScanStatus,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const axiosClient = axios.create({});
|
const axiosClient = axios.create({});
|
||||||
|
|||||||
@@ -2461,6 +2461,17 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
startScan: async (args) => {
|
||||||
|
const { apiClientProps } = args;
|
||||||
|
|
||||||
|
const res = await ssApiClient(apiClientProps).startScan({ query: {} });
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to start scan');
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.body.scanStatus;
|
||||||
|
},
|
||||||
updateInternetRadioStation: async (args) => {
|
updateInternetRadioStation: async (args) => {
|
||||||
const { apiClientProps, body, query } = args;
|
const { apiClientProps, body, query } = args;
|
||||||
|
|
||||||
@@ -2497,17 +2508,6 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
startScan: async (args) => {
|
|
||||||
const { apiClientProps } = args;
|
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).startScan({ query: {} });
|
|
||||||
|
|
||||||
if (res.status !== 200) {
|
|
||||||
throw new Error('Failed to start scan');
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.body.scanStatus;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function getLibraryId(musicFolderId?: string | string[]) {
|
function getLibraryId(musicFolderId?: string | string[]) {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
|
import { openModal } from '@mantine/modals';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { openModal } from '@mantine/modals';
|
|
||||||
import { controller } from '/@/renderer/api/controller';
|
import { controller } from '/@/renderer/api/controller';
|
||||||
|
import { SongEditModal } from '/@/renderer/features/tag-editor/components/song-edit-modal';
|
||||||
import { useCurrentServer } from '/@/renderer/store';
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
import { ContextMenu } from '/@/shared/components/context-menu/context-menu';
|
import { ContextMenu } from '/@/shared/components/context-menu/context-menu';
|
||||||
import { Song } from '/@/shared/types/domain-types';
|
import { Song } from '/@/shared/types/domain-types';
|
||||||
import { SongEditModal } from '/@/renderer/features/tag-editor/components/song-edit-modal';
|
|
||||||
|
|
||||||
interface EditMetadataActionProps {
|
interface EditMetadataActionProps {
|
||||||
albumIds?: string[];
|
albumIds?: string[];
|
||||||
@@ -31,7 +31,7 @@ const getAlbumSongs = async (albumIds: string[], serverId: string): Promise<Song
|
|||||||
export const EditMetadataAction = ({ albumIds, songs: songItems }: EditMetadataActionProps) => {
|
export const EditMetadataAction = ({ albumIds, songs: songItems }: EditMetadataActionProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const songs = songItems?.filter((s) => s.path) ?? [];
|
const songs = useMemo(() => songItems?.filter((s) => s.path) ?? [], [songItems]);
|
||||||
const count = albumIds?.length ?? songs.length;
|
const count = albumIds?.length ?? songs.length;
|
||||||
|
|
||||||
const onSelect = useCallback(async () => {
|
const onSelect = useCallback(async () => {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { AddToPlaylistAction } from '/@/renderer/features/context-menu/actions/add-to-playlist-action';
|
import { AddToPlaylistAction } from '/@/renderer/features/context-menu/actions/add-to-playlist-action';
|
||||||
import { EditMetadataAction } from '/@/renderer/features/context-menu/actions/edit-metadata-action';
|
|
||||||
import { DownloadAction } from '/@/renderer/features/context-menu/actions/download-action';
|
import { DownloadAction } from '/@/renderer/features/context-menu/actions/download-action';
|
||||||
|
import { EditMetadataAction } from '/@/renderer/features/context-menu/actions/edit-metadata-action';
|
||||||
import { GetInfoAction } from '/@/renderer/features/context-menu/actions/get-info-action';
|
import { GetInfoAction } from '/@/renderer/features/context-menu/actions/get-info-action';
|
||||||
import { GoToAction } from '/@/renderer/features/context-menu/actions/go-to-action';
|
import { GoToAction } from '/@/renderer/features/context-menu/actions/go-to-action';
|
||||||
import { PlayAction } from '/@/renderer/features/context-menu/actions/play-action';
|
import { PlayAction } from '/@/renderer/features/context-menu/actions/play-action';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { AddToPlaylistAction } from '/@/renderer/features/context-menu/actions/add-to-playlist-action';
|
import { AddToPlaylistAction } from '/@/renderer/features/context-menu/actions/add-to-playlist-action';
|
||||||
import { EditMetadataAction } from '/@/renderer/features/context-menu/actions/edit-metadata-action';
|
|
||||||
import { DownloadAction } from '/@/renderer/features/context-menu/actions/download-action';
|
import { DownloadAction } from '/@/renderer/features/context-menu/actions/download-action';
|
||||||
|
import { EditMetadataAction } from '/@/renderer/features/context-menu/actions/edit-metadata-action';
|
||||||
import { GetInfoAction } from '/@/renderer/features/context-menu/actions/get-info-action';
|
import { GetInfoAction } from '/@/renderer/features/context-menu/actions/get-info-action';
|
||||||
import { GoToAction } from '/@/renderer/features/context-menu/actions/go-to-action';
|
import { GoToAction } from '/@/renderer/features/context-menu/actions/go-to-action';
|
||||||
import { PlayAction } from '/@/renderer/features/context-menu/actions/play-action';
|
import { PlayAction } from '/@/renderer/features/context-menu/actions/play-action';
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
.artworkBox {
|
.artwork-box {
|
||||||
aspect-ratio: 1 / 1;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
container-type: inline-size;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: var(--mantine-color-dark-6);
|
background: var(--mantine-color-dark-6);
|
||||||
border: 1px solid var(--mantine-color-default-border);
|
border: 1px solid var(--mantine-color-default-border);
|
||||||
border-radius: var(--mantine-radius-sm);
|
border-radius: var(--mantine-radius-sm);
|
||||||
container-type: inline-size;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.artworkImage {
|
.artwork-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
@@ -20,10 +20,10 @@
|
|||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholderText {
|
.placeholder-text {
|
||||||
font-size: 5cqw;
|
font-size: 5cqw;
|
||||||
}
|
}
|
||||||
|
|
||||||
.removeButton {
|
.remove-button {
|
||||||
color: var(--theme-colors-state-error);
|
color: var(--theme-colors-state-error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { DragDropZone } from '/@/shared/components/drag-drop-zone/drag-drop-zone';
|
import styles from './artwork-panel.module.css';
|
||||||
|
|
||||||
import { Button } from '/@/shared/components/button/button';
|
import { Button } from '/@/shared/components/button/button';
|
||||||
|
import { DragDropZone } from '/@/shared/components/drag-drop-zone/drag-drop-zone';
|
||||||
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';
|
||||||
|
|
||||||
import styles from './artwork-panel.module.css';
|
|
||||||
|
|
||||||
interface ArtworkPanelProps {
|
interface ArtworkPanelProps {
|
||||||
artworkDisplayUrl: null | string;
|
artworkDisplayUrl: null | string;
|
||||||
artworkIsMixed: boolean;
|
artworkIsMixed: boolean;
|
||||||
@@ -49,12 +49,7 @@ export const ArtworkPanel = ({
|
|||||||
)}
|
)}
|
||||||
</DragDropZone>
|
</DragDropZone>
|
||||||
{showRemoveButton && (
|
{showRemoveButton && (
|
||||||
<Button
|
<Button className={styles.removeButton} onClick={onRemove} size="sm" variant="subtle">
|
||||||
className={styles.removeButton}
|
|
||||||
onClick={onRemove}
|
|
||||||
size="sm"
|
|
||||||
variant="subtle"
|
|
||||||
>
|
|
||||||
{removeArtworkLabel}
|
{removeArtworkLabel}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
.tabLabel {
|
.tab-label {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tableScroller {
|
.table-scroller {
|
||||||
max-height: 55vh;
|
max-height: 55vh;
|
||||||
overflow-y: auto;
|
|
||||||
margin-top: var(--theme-spacing-sm);
|
|
||||||
padding-top: var(--theme-spacing-sm);
|
padding-top: var(--theme-spacing-sm);
|
||||||
padding-bottom: var(--theme-spacing-sm);
|
padding-bottom: var(--theme-spacing-sm);
|
||||||
|
margin-top: var(--theme-spacing-sm);
|
||||||
|
overflow-y: auto;
|
||||||
border-top: 1px solid var(--mantine-color-default-border);
|
border-top: 1px solid var(--mantine-color-default-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tableCell {
|
.table-cell {
|
||||||
padding: var(--theme-spacing-xs) var(--theme-spacing-sm);
|
padding: var(--theme-spacing-xs) var(--theme-spacing-sm);
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tableHeader {
|
.table-header {
|
||||||
width: 1%;
|
width: 1%;
|
||||||
color: var(--theme-colors-foreground-muted);
|
|
||||||
font-weight: 500;
|
|
||||||
padding: var(--theme-spacing-xs) var(--theme-spacing-sm);
|
padding: var(--theme-spacing-xs) var(--theme-spacing-sm);
|
||||||
|
font-weight: 500;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
color: var(--theme-colors-foreground-muted);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { closeAllModals } from '@mantine/modals';
|
|||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { useMetadataEditor } from '../hooks/use-metadata-editor';
|
||||||
|
import { ArtworkPanel } from './artwork-panel';
|
||||||
import styles from './song-edit-modal.module.css';
|
import styles from './song-edit-modal.module.css';
|
||||||
|
import { TagFieldRow } from './tag-field-row';
|
||||||
|
|
||||||
import { Button } from '/@/shared/components/button/button';
|
import { Button } from '/@/shared/components/button/button';
|
||||||
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
|
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
|
||||||
@@ -15,10 +18,6 @@ import { Tabs } from '/@/shared/components/tabs/tabs';
|
|||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { Song } from '/@/shared/types/domain-types';
|
import { Song } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
import { useMetadataEditor } from '../hooks/use-metadata-editor';
|
|
||||||
import { ArtworkPanel } from './artwork-panel';
|
|
||||||
import { TagFieldRow } from './tag-field-row';
|
|
||||||
|
|
||||||
export const SongEditModal = ({ songs }: { songs: Song[] }) => {
|
export const SongEditModal = ({ songs }: { songs: Song[] }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
.removeCell {
|
.remove-cell {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.removeButton {
|
.remove-button {
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { RiCloseLine } from 'react-icons/ri';
|
import { RiCloseLine } from 'react-icons/ri';
|
||||||
|
|
||||||
|
import type { KnownTag } from '../utils/known-tags';
|
||||||
|
|
||||||
|
import styles from './tag-field-row.module.css';
|
||||||
|
|
||||||
import { Button } from '/@/shared/components/button/button';
|
import { Button } from '/@/shared/components/button/button';
|
||||||
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
|
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
|
||||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||||
import { Table } from '/@/shared/components/table/table';
|
import { Table } from '/@/shared/components/table/table';
|
||||||
import { Textarea } from '/@/shared/components/textarea/textarea';
|
|
||||||
import { TextInput } from '/@/shared/components/text-input/text-input';
|
import { TextInput } from '/@/shared/components/text-input/text-input';
|
||||||
|
import { Textarea } from '/@/shared/components/textarea/textarea';
|
||||||
import type { KnownTag } from '../utils/known-tags';
|
|
||||||
|
|
||||||
import styles from './tag-field-row.module.css';
|
|
||||||
|
|
||||||
interface TagFieldRowProps {
|
interface TagFieldRowProps {
|
||||||
isMixed: boolean;
|
isMixed: boolean;
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import type {
|
|||||||
BatchProgress,
|
BatchProgress,
|
||||||
TagEditorUtils,
|
TagEditorUtils,
|
||||||
} from '/@/shared/types/tag-editor';
|
} from '/@/shared/types/tag-editor';
|
||||||
|
|
||||||
import { closeAllModals } from '@mantine/modals';
|
import { closeAllModals } from '@mantine/modals';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { FIELD_PRIORITY, KNOWN_TAG_MAP, KNOWN_TAGS, type KnownTag } from '../utils/known-tags';
|
import { FIELD_PRIORITY, KNOWN_TAG_MAP, KNOWN_TAGS, type KnownTag } from '../utils/known-tags';
|
||||||
import { base64ToBytes, filterTagSummary, formatBatchFileErrors } from '../utils/utils';
|
import { base64ToBytes, filterTagSummary, formatBatchFileErrors } from '../utils/utils';
|
||||||
|
|
||||||
import { controller } from '/@/renderer/api/controller';
|
import { controller } from '/@/renderer/api/controller';
|
||||||
import { useCurrentServer } from '/@/renderer/store';
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
|
|||||||
@@ -49,15 +49,15 @@ const FIELD_TYPE_OVERRIDES: Partial<
|
|||||||
lyrics: 'textarea',
|
lyrics: 'textarea',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Which form control to render for a tag row. */
|
|
||||||
export type TagFieldType = 'boolean' | 'number' | 'string' | 'textarea';
|
|
||||||
|
|
||||||
export interface KnownTag {
|
export interface KnownTag {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
type: TagFieldType;
|
type: TagFieldType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Which form control to render for a tag row. */
|
||||||
|
export type TagFieldType = 'boolean' | 'number' | 'string' | 'textarea';
|
||||||
|
|
||||||
const humanizeKey = (key: string): string =>
|
const humanizeKey = (key: string): string =>
|
||||||
key
|
key
|
||||||
.replace(/([A-Z])/g, ' $1')
|
.replace(/([A-Z])/g, ' $1')
|
||||||
|
|||||||
@@ -842,6 +842,7 @@ export const ssType = {
|
|||||||
getMusicDirectory: getMusicDirectoryParameters,
|
getMusicDirectory: getMusicDirectoryParameters,
|
||||||
getPlaylist: getPlaylistParameters,
|
getPlaylist: getPlaylistParameters,
|
||||||
getPlaylists: getPlaylistsParameters,
|
getPlaylists: getPlaylistsParameters,
|
||||||
|
getScanStatus: getScanStatusParameters,
|
||||||
getSong: getSongParameters,
|
getSong: getSongParameters,
|
||||||
getSongsByGenre: getSongsByGenreParameters,
|
getSongsByGenre: getSongsByGenreParameters,
|
||||||
getStarred: getStarredParameters,
|
getStarred: getStarredParameters,
|
||||||
@@ -852,13 +853,12 @@ export const ssType = {
|
|||||||
reportPlayback: reportPlaybackParameters,
|
reportPlayback: reportPlaybackParameters,
|
||||||
savePlayQueueByIndex: savePlayQueueByIndexParameters,
|
savePlayQueueByIndex: savePlayQueueByIndexParameters,
|
||||||
saveQueue: saveQueueParameters,
|
saveQueue: saveQueueParameters,
|
||||||
getScanStatus: getScanStatusParameters,
|
|
||||||
scrobble: scrobbleParameters,
|
scrobble: scrobbleParameters,
|
||||||
search3: search3Parameters,
|
search3: search3Parameters,
|
||||||
startScan: startScanParameters,
|
|
||||||
setRating: setRatingParameters,
|
setRating: setRatingParameters,
|
||||||
similarSongs: similarSongsParameters,
|
similarSongs: similarSongsParameters,
|
||||||
similarSongs2: similarSongs2Parameters,
|
similarSongs2: similarSongs2Parameters,
|
||||||
|
startScan: startScanParameters,
|
||||||
structuredLyrics: structuredLyricsParameters,
|
structuredLyrics: structuredLyricsParameters,
|
||||||
topSongsList: topSongsListParameters,
|
topSongsList: topSongsListParameters,
|
||||||
updateInternetRadioStation: updateInternetRadioStationParameters,
|
updateInternetRadioStation: updateInternetRadioStationParameters,
|
||||||
@@ -892,6 +892,7 @@ export const ssType = {
|
|||||||
getMusicDirectory,
|
getMusicDirectory,
|
||||||
getPlaylist,
|
getPlaylist,
|
||||||
getPlaylists,
|
getPlaylists,
|
||||||
|
getScanStatus,
|
||||||
getSong,
|
getSong,
|
||||||
getSongsByGenre,
|
getSongsByGenre,
|
||||||
getStarred,
|
getStarred,
|
||||||
@@ -907,15 +908,14 @@ export const ssType = {
|
|||||||
removeFavorite,
|
removeFavorite,
|
||||||
reportPlayback,
|
reportPlayback,
|
||||||
saveQueue,
|
saveQueue,
|
||||||
getScanStatus,
|
|
||||||
scrobble,
|
scrobble,
|
||||||
search3,
|
search3,
|
||||||
startScan,
|
|
||||||
serverInfo,
|
serverInfo,
|
||||||
setRating,
|
setRating,
|
||||||
similarSongs,
|
similarSongs,
|
||||||
similarSongs2,
|
similarSongs2,
|
||||||
song,
|
song,
|
||||||
|
startScan,
|
||||||
structuredLyrics,
|
structuredLyrics,
|
||||||
topSongsList,
|
topSongsList,
|
||||||
updateInternetRadioStation,
|
updateInternetRadioStation,
|
||||||
|
|||||||
@@ -1376,16 +1376,6 @@ export type ScrobbleQuery = {
|
|||||||
// Scrobble
|
// Scrobble
|
||||||
export type ScrobbleResponse = null;
|
export type ScrobbleResponse = null;
|
||||||
|
|
||||||
export type StartScanArgs = BaseEndpointArgs;
|
|
||||||
|
|
||||||
export type StartScanResponse = {
|
|
||||||
count: number;
|
|
||||||
folderCount: number;
|
|
||||||
lastScan?: string;
|
|
||||||
scanning: boolean;
|
|
||||||
} | null;
|
|
||||||
|
|
||||||
|
|
||||||
export type SearchAlbumArtistsQuery = {
|
export type SearchAlbumArtistsQuery = {
|
||||||
albumArtistLimit?: number;
|
albumArtistLimit?: number;
|
||||||
albumArtistStartIndex?: number;
|
albumArtistStartIndex?: number;
|
||||||
@@ -1428,6 +1418,15 @@ export type SearchSongsQuery = {
|
|||||||
songStartIndex?: number;
|
songStartIndex?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type StartScanArgs = BaseEndpointArgs;
|
||||||
|
|
||||||
|
export type StartScanResponse = null | {
|
||||||
|
count: number;
|
||||||
|
folderCount: number;
|
||||||
|
lastScan?: string;
|
||||||
|
scanning: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type SynchronizedLyricsArray = Array<[number, string]>;
|
export type SynchronizedLyricsArray = Array<[number, string]>;
|
||||||
|
|
||||||
export type TopSongListArgs = BaseEndpointArgs & { query: TopSongListQuery };
|
export type TopSongListArgs = BaseEndpointArgs & { query: TopSongListQuery };
|
||||||
@@ -1541,6 +1540,7 @@ export type ControllerEndpoint = {
|
|||||||
setPlaylistSongs: (args: SetPlaylistSongsArgs) => Promise<SetPlaylistSongsResponse>;
|
setPlaylistSongs: (args: SetPlaylistSongsArgs) => Promise<SetPlaylistSongsResponse>;
|
||||||
setRating?: (args: SetRatingArgs) => Promise<RatingResponse>;
|
setRating?: (args: SetRatingArgs) => Promise<RatingResponse>;
|
||||||
shareItem?: (args: ShareItemArgs) => Promise<ShareItemResponse>;
|
shareItem?: (args: ShareItemArgs) => Promise<ShareItemResponse>;
|
||||||
|
startScan: (args: StartScanArgs) => Promise<StartScanResponse>;
|
||||||
updateInternetRadioStation: (
|
updateInternetRadioStation: (
|
||||||
args: UpdateInternetRadioStationArgs,
|
args: UpdateInternetRadioStationArgs,
|
||||||
) => Promise<UpdateInternetRadioStationResponse>;
|
) => Promise<UpdateInternetRadioStationResponse>;
|
||||||
@@ -1550,7 +1550,6 @@ export type ControllerEndpoint = {
|
|||||||
args: UploadInternetRadioStationImageArgs,
|
args: UploadInternetRadioStationImageArgs,
|
||||||
) => Promise<UploadInternetRadioStationImageResponse>;
|
) => Promise<UploadInternetRadioStationImageResponse>;
|
||||||
uploadPlaylistImage?: (args: UploadPlaylistImageArgs) => Promise<UploadPlaylistImageResponse>;
|
uploadPlaylistImage?: (args: UploadPlaylistImageArgs) => Promise<UploadPlaylistImageResponse>;
|
||||||
startScan: (args: StartScanArgs) => Promise<StartScanResponse>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DownloadArgs = BaseEndpointArgs & {
|
export type DownloadArgs = BaseEndpointArgs & {
|
||||||
@@ -1710,6 +1709,7 @@ export type InternalControllerEndpoint = {
|
|||||||
) => Promise<SetPlaylistSongsResponse>;
|
) => Promise<SetPlaylistSongsResponse>;
|
||||||
setRating?: (args: ReplaceApiClientProps<SetRatingArgs>) => Promise<RatingResponse>;
|
setRating?: (args: ReplaceApiClientProps<SetRatingArgs>) => Promise<RatingResponse>;
|
||||||
shareItem?: (args: ReplaceApiClientProps<ShareItemArgs>) => Promise<ShareItemResponse>;
|
shareItem?: (args: ReplaceApiClientProps<ShareItemArgs>) => Promise<ShareItemResponse>;
|
||||||
|
startScan: (args: ReplaceApiClientProps<StartScanArgs>) => Promise<StartScanResponse>;
|
||||||
updateInternetRadioStation: (
|
updateInternetRadioStation: (
|
||||||
args: ReplaceApiClientProps<UpdateInternetRadioStationArgs>,
|
args: ReplaceApiClientProps<UpdateInternetRadioStationArgs>,
|
||||||
) => Promise<UpdateInternetRadioStationResponse>;
|
) => Promise<UpdateInternetRadioStationResponse>;
|
||||||
@@ -1725,7 +1725,6 @@ export type InternalControllerEndpoint = {
|
|||||||
uploadPlaylistImage?: (
|
uploadPlaylistImage?: (
|
||||||
args: ReplaceApiClientProps<UploadPlaylistImageArgs>,
|
args: ReplaceApiClientProps<UploadPlaylistImageArgs>,
|
||||||
) => Promise<UploadPlaylistImageResponse>;
|
) => Promise<UploadPlaylistImageResponse>;
|
||||||
startScan: (args: ReplaceApiClientProps<StartScanArgs>) => Promise<StartScanResponse>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LyricGetQuery = {
|
export type LyricGetQuery = {
|
||||||
|
|||||||
@@ -48,8 +48,6 @@ export const EDITOR_FIELD_KEYS = [
|
|||||||
'acoustidId',
|
'acoustidId',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type EditorFieldKey = (typeof EDITOR_FIELD_KEYS)[number];
|
|
||||||
|
|
||||||
export type ArtworkKind = 'common' | 'mixed' | 'none';
|
export type ArtworkKind = 'common' | 'mixed' | 'none';
|
||||||
|
|
||||||
export type ArtworkOp = { bytes: Uint8Array; mimeType: string; type: 'set' } | { type: 'clear' };
|
export type ArtworkOp = { bytes: Uint8Array; mimeType: string; type: 'set' } | { type: 'clear' };
|
||||||
@@ -64,10 +62,7 @@ export interface BatchProgress {
|
|||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IpcResult {
|
export type EditorFieldKey = (typeof EDITOR_FIELD_KEYS)[number];
|
||||||
error?: string;
|
|
||||||
success: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReadLocalImageResult extends IpcResult {
|
export interface ReadLocalImageResult extends IpcResult {
|
||||||
data?: string;
|
data?: string;
|
||||||
@@ -84,10 +79,6 @@ export interface ReadSongMetadataBatchResult extends IpcResult {
|
|||||||
totalCount?: number;
|
totalCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WriteSongTagsBatchResult extends IpcResult {
|
|
||||||
failedFiles?: BatchFileError[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Subset of `window.api.utils` consumed by the metadata editor. */
|
/** Subset of `window.api.utils` consumed by the metadata editor. */
|
||||||
export interface TagEditorUtils {
|
export interface TagEditorUtils {
|
||||||
cancelReadSongMetadata: () => void;
|
cancelReadSongMetadata: () => void;
|
||||||
@@ -102,3 +93,12 @@ export interface TagEditorUtils {
|
|||||||
artworkOp?: ArtworkOp,
|
artworkOp?: ArtworkOp,
|
||||||
) => Promise<WriteSongTagsBatchResult>;
|
) => Promise<WriteSongTagsBatchResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WriteSongTagsBatchResult extends IpcResult {
|
||||||
|
failedFiles?: BatchFileError[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IpcResult {
|
||||||
|
error?: string;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user