mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-01 02:13:33 +00:00
Add localization support (#333)
* Add updated i18n config and en locale
This commit is contained in:
parent
11863fd4c1
commit
8430b1ec95
90 changed files with 2679 additions and 908 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
import { Box, Group, Stack } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { closeModal, ContextModalProps } from '@mantine/modals';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { PlaylistListSort, SongListQuery, SongListSort, SortOrder } from '/@/renderer/api/types';
|
||||
|
|
@ -11,6 +11,7 @@ import { useAddToPlaylist } from '/@/renderer/features/playlists/mutations/add-t
|
|||
import { usePlaylistList } from '/@/renderer/features/playlists/queries/playlist-list-query';
|
||||
import { queryClient } from '/@/renderer/lib/react-query';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const AddToPlaylistContextModal = ({
|
||||
id,
|
||||
|
|
@ -21,6 +22,7 @@ export const AddToPlaylistContextModal = ({
|
|||
genreId?: string[];
|
||||
songId?: string[];
|
||||
}>) => {
|
||||
const { t } = useTranslation();
|
||||
const { albumId, artistId, genreId, songId } = innerProps;
|
||||
const server = useCurrentServer();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
|
@ -140,7 +142,10 @@ export const AddToPlaylistContextModal = ({
|
|||
const queryKey = queryKeys.playlists.songList(server?.id || '', playlistId, query);
|
||||
|
||||
const playlistSongsRes = await queryClient.fetchQuery(queryKey, ({ signal }) => {
|
||||
if (!server) throw new Error('No server');
|
||||
if (!server)
|
||||
throw new Error(
|
||||
t('error.serverNotSelectedError', { postProcess: 'sentenceCase' }),
|
||||
);
|
||||
return api.controller.getPlaylistSongList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
|
|
@ -175,7 +180,7 @@ export const AddToPlaylistContextModal = ({
|
|||
playlistSelect.find((playlist) => playlist.value === playlistId)
|
||||
?.label
|
||||
}] ${err.message}`,
|
||||
title: 'Failed to add songs to playlist',
|
||||
title: t('error.genericError', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
@ -186,12 +191,16 @@ export const AddToPlaylistContextModal = ({
|
|||
const addMessage =
|
||||
values.skipDuplicates &&
|
||||
allSongIds.length * values.playlistId.length !== totalUniquesAdded
|
||||
? `around ${Math.floor(totalUniquesAdded / values.playlistId.length)}`
|
||||
? `${Math.floor(totalUniquesAdded / values.playlistId.length)}`
|
||||
: allSongIds.length;
|
||||
|
||||
setIsLoading(false);
|
||||
toast.success({
|
||||
message: `Added ${addMessage} songs to ${values.playlistId.length} playlist(s)`,
|
||||
message: t('form.addToPlaylist', {
|
||||
message: addMessage,
|
||||
numOfPlaylists: values.playlistId.length,
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
});
|
||||
closeModal(id);
|
||||
return null;
|
||||
|
|
@ -206,12 +215,18 @@ export const AddToPlaylistContextModal = ({
|
|||
searchable
|
||||
data={playlistSelect}
|
||||
disabled={playlistList.isLoading}
|
||||
label="Playlists"
|
||||
label={t('form.addToPlaylist.input', {
|
||||
context: 'playlists',
|
||||
postProcess: 'titleCase',
|
||||
})}
|
||||
size="md"
|
||||
{...form.getInputProps('playlistId')}
|
||||
/>
|
||||
<Switch
|
||||
label="Skip duplicates"
|
||||
label={t('form.addToPlaylist.input', {
|
||||
context: 'skipDuplicates',
|
||||
postProcess: 'titleCase',
|
||||
})}
|
||||
{...form.getInputProps('skipDuplicates', { type: 'checkbox' })}
|
||||
/>
|
||||
<Group position="right">
|
||||
|
|
@ -222,7 +237,7 @@ export const AddToPlaylistContextModal = ({
|
|||
variant="subtle"
|
||||
onClick={() => closeModal(id)}
|
||||
>
|
||||
Cancel
|
||||
{t('common.cancel', { postProcess: 'titleCase' })}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isSubmitDisabled}
|
||||
|
|
@ -231,7 +246,7 @@ export const AddToPlaylistContextModal = ({
|
|||
type="submit"
|
||||
variant="filled"
|
||||
>
|
||||
Add
|
||||
{t('common.add', { postProcess: 'titleCase' })}
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useRef, useState } from 'react';
|
||||
import { Group, Stack } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useRef, useState } from 'react';
|
||||
import { CreatePlaylistBody, ServerType, SongListSort } from '/@/renderer/api/types';
|
||||
import { Button, Switch, Text, TextInput, toast } from '/@/renderer/components';
|
||||
import {
|
||||
|
|
@ -10,12 +10,14 @@ import {
|
|||
import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/create-playlist-mutation';
|
||||
import { convertQueryGroupToNDQuery } from '/@/renderer/features/playlists/utils';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface CreatePlaylistFormProps {
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
|
||||
const { t } = useTranslation();
|
||||
const mutation = useCreatePlaylist({});
|
||||
const server = useCurrentServer();
|
||||
const queryBuilderRef = useRef<PlaylistQueryBuilderRef>(null);
|
||||
|
|
@ -69,10 +71,15 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
|
|||
},
|
||||
{
|
||||
onError: (err) => {
|
||||
toast.error({ message: err.message, title: 'Error creating playlist' });
|
||||
toast.error({
|
||||
message: err.message,
|
||||
title: t('error.genericError', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success({ message: `Playlist has been created` });
|
||||
toast.success({
|
||||
message: t('form.createPlaylist.success', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
onCancel();
|
||||
},
|
||||
},
|
||||
|
|
@ -88,17 +95,26 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
|
|||
<TextInput
|
||||
data-autofocus
|
||||
required
|
||||
label="Name"
|
||||
label={t('form.createPlaylist.input', {
|
||||
context: 'name',
|
||||
postProcess: 'titleCase',
|
||||
})}
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
<TextInput
|
||||
label="Description"
|
||||
label={t('form.createPlaylist.input', {
|
||||
context: 'description',
|
||||
postProcess: 'titleCase',
|
||||
})}
|
||||
{...form.getInputProps('comment')}
|
||||
/>
|
||||
<Group>
|
||||
{isPublicDisplayed && (
|
||||
<Switch
|
||||
label="Is public?"
|
||||
label={t('form.createPlaylist.input', {
|
||||
context: 'public',
|
||||
postProcess: 'titleCase',
|
||||
})}
|
||||
{...form.getInputProps('_custom.navidrome.public', {
|
||||
type: 'checkbox',
|
||||
})}
|
||||
|
|
@ -130,7 +146,7 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
|
|||
variant="subtle"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
{t('common.cancel', { postProcess: 'titleCase' })}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isSubmitDisabled}
|
||||
|
|
@ -138,7 +154,7 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
|
|||
type="submit"
|
||||
variant="filled"
|
||||
>
|
||||
Save
|
||||
{t('common.create', { postProcess: 'titleCase' })}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { MutableRefObject, useMemo, useRef } from 'react';
|
||||
import { ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import { Box, Group } from '@mantine/core';
|
||||
import { closeAllModals, openModal } from '@mantine/modals';
|
||||
import { MutableRefObject, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RiMoreFill } from 'react-icons/ri';
|
||||
import { generatePath, useNavigate, useParams } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
|
@ -45,6 +46,7 @@ interface PlaylistDetailContentProps {
|
|||
}
|
||||
|
||||
export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { playlistId } = useParams() as { playlistId: string };
|
||||
const { table } = useListStoreByKey({ key: LibraryItem.SONG });
|
||||
|
|
@ -102,13 +104,10 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps)
|
|||
onError: (err) => {
|
||||
toast.error({
|
||||
message: err.message,
|
||||
title: 'Error deleting playlist',
|
||||
title: t('error.genericError', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success({
|
||||
message: `Playlist has been deleted`,
|
||||
});
|
||||
closeAllModals();
|
||||
navigate(AppRoute.PLAYLISTS);
|
||||
},
|
||||
|
|
@ -126,7 +125,7 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps)
|
|||
Are you sure you want to delete this playlist?
|
||||
</ConfirmModal>
|
||||
),
|
||||
title: 'Delete playlist',
|
||||
title: t('form.deletePlaylist.title', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
|
|||
import { Divider, Flex, Group, Stack } from '@mantine/core';
|
||||
import { closeAllModals, openModal } from '@mantine/modals';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
RiMoreFill,
|
||||
RiSettings3Fill,
|
||||
|
|
@ -101,6 +102,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||
tableRef,
|
||||
handleToggleShowQueryBuilder,
|
||||
}: PlaylistDetailSongListHeaderFiltersProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { playlistId } = useParams() as { playlistId: string };
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
|
@ -267,19 +269,16 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||
onError: (err) => {
|
||||
toast.error({
|
||||
message: err.message,
|
||||
title: 'Error deleting playlist',
|
||||
title: t('error.genericError', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success({
|
||||
message: `Playlist has been deleted`,
|
||||
});
|
||||
navigate(AppRoute.PLAYLISTS, { replace: true });
|
||||
},
|
||||
},
|
||||
);
|
||||
closeAllModals();
|
||||
}, [deletePlaylistMutation, detailQuery.data, navigate]);
|
||||
}, [deletePlaylistMutation, detailQuery.data, navigate, t]);
|
||||
|
||||
const openDeletePlaylistModal = () => {
|
||||
openModal({
|
||||
|
|
@ -288,7 +287,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||
<Text>Are you sure you want to delete this playlist?</Text>
|
||||
</ConfirmModal>
|
||||
),
|
||||
title: 'Delete playlist(s)',
|
||||
title: t('form.deletePlaylist.title', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -345,19 +344,19 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||
icon={<RiPlayFill />}
|
||||
onClick={() => handlePlay(Play.NOW)}
|
||||
>
|
||||
Play
|
||||
{t('player.play', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
icon={<RiAddBoxFill />}
|
||||
onClick={() => handlePlay(Play.LAST)}
|
||||
>
|
||||
Add to queue
|
||||
{t('player.addLast', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
icon={<RiAddCircleFill />}
|
||||
onClick={() => handlePlay(Play.NEXT)}
|
||||
>
|
||||
Add to queue next
|
||||
{t('player.addNext', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Item
|
||||
|
|
@ -369,20 +368,20 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||
})
|
||||
}
|
||||
>
|
||||
Edit playlist
|
||||
{t('action.editPlaylist', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
icon={<RiDeleteBinFill />}
|
||||
onClick={openDeletePlaylistModal}
|
||||
>
|
||||
Delete playlist
|
||||
{t('action.deletePlaylist', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Item
|
||||
icon={<RiRefreshLine />}
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
Refresh
|
||||
{t('action.refresh', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
{server?.type === ServerType.NAVIDROME && !isSmartPlaylist && (
|
||||
<>
|
||||
|
|
@ -391,7 +390,9 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||
$danger
|
||||
onClick={handleToggleShowQueryBuilder}
|
||||
>
|
||||
Toggle smart playlist editor
|
||||
{t('action.toggleSmartPlaylistEditor', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</DropdownMenu.Item>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { MutableRefObject } from 'react';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import { Stack } from '@mantine/core';
|
||||
import { MutableRefObject } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router';
|
||||
import { LibraryItem } from '/@/renderer/api/types';
|
||||
import { Badge, PageHeader, Paper, SpinnerIcon } from '/@/renderer/components';
|
||||
|
|
@ -23,6 +24,7 @@ export const PlaylistDetailSongListHeader = ({
|
|||
itemCount,
|
||||
handleToggleShowQueryBuilder,
|
||||
}: PlaylistDetailHeaderProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { playlistId } = useParams() as { playlistId: string };
|
||||
const server = useCurrentServer();
|
||||
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id });
|
||||
|
|
@ -58,7 +60,7 @@ export const PlaylistDetailSongListHeader = ({
|
|||
itemCount
|
||||
)}
|
||||
</Paper>
|
||||
{isSmartPlaylist && <Badge size="lg">Smart playlist</Badge>}
|
||||
{isSmartPlaylist && <Badge size="lg">{t('entity.smartPlaylist')}</Badge>}
|
||||
</LibraryHeaderBar>
|
||||
</PageHeader>
|
||||
<Paper p="1rem">
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { ChangeEvent, MouseEvent, MutableRefObject, useCallback } from 'react';
|
||||
import { IDatasource } from '@ag-grid-community/core';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import { Divider, Flex, Group, Stack } from '@mantine/core';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { ChangeEvent, MouseEvent, MutableRefObject, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RiMoreFill, RiRefreshLine, RiSettings3Fill } from 'react-icons/ri';
|
||||
import { useListContext } from '../../../context/list-context';
|
||||
import { useListStoreByKey } from '../../../store/list.store';
|
||||
|
|
@ -42,6 +43,7 @@ export const PlaylistListHeaderFilters = ({
|
|||
gridRef,
|
||||
tableRef,
|
||||
}: PlaylistListHeaderFiltersProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { pageKey } = useListContext();
|
||||
const queryClient = useQueryClient();
|
||||
const server = useCurrentServer();
|
||||
|
|
@ -285,7 +287,7 @@ export const PlaylistListHeaderFilters = ({
|
|||
<Button
|
||||
compact
|
||||
size="md"
|
||||
tooltip={{ label: 'Refresh' }}
|
||||
tooltip={{ label: t('common.refresh', { postProcess: 'titleCase' }) }}
|
||||
variant="subtle"
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
|
|
@ -308,7 +310,7 @@ export const PlaylistListHeaderFilters = ({
|
|||
icon={<RiRefreshLine />}
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
Refresh
|
||||
{t('common.refresh', { postProcess: 'titleCase' })}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
|
|
@ -328,27 +330,29 @@ export const PlaylistListHeaderFilters = ({
|
|||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
<DropdownMenu.Label>Display type</DropdownMenu.Label>
|
||||
<DropdownMenu.Label>
|
||||
{t('table.config.general.displayType', { postProcess: 'titleCase' })}
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Item
|
||||
$isActive={display === ListDisplayType.CARD}
|
||||
value={ListDisplayType.CARD}
|
||||
onClick={handleSetViewType}
|
||||
>
|
||||
Card
|
||||
{t('table.config.view.card', { postProcess: 'titleCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
$isActive={display === ListDisplayType.POSTER}
|
||||
value={ListDisplayType.POSTER}
|
||||
onClick={handleSetViewType}
|
||||
>
|
||||
Poster
|
||||
{t('table.config.view.poster', { postProcess: 'titleCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
$isActive={display === ListDisplayType.TABLE}
|
||||
value={ListDisplayType.TABLE}
|
||||
onClick={handleSetViewType}
|
||||
>
|
||||
Table
|
||||
{t('table.config.view.table', { postProcess: 'titleCase' })}
|
||||
</DropdownMenu.Item>
|
||||
{/* <DropdownMenu.Item
|
||||
$isActive={display === ListDisplayType.TABLE_PAGINATED}
|
||||
|
|
@ -382,7 +386,11 @@ export const PlaylistListHeaderFilters = ({
|
|||
</DropdownMenu.Item>
|
||||
{!isGrid && (
|
||||
<>
|
||||
<DropdownMenu.Label>Table Columns</DropdownMenu.Label>
|
||||
<DropdownMenu.Label>
|
||||
{t('table.config.generaltableColumns', {
|
||||
postProcess: 'titleCase',
|
||||
})}
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Item
|
||||
closeMenuOnClick={false}
|
||||
component="div"
|
||||
|
|
@ -399,7 +407,11 @@ export const PlaylistListHeaderFilters = ({
|
|||
onChange={handleTableColumns}
|
||||
/>
|
||||
<Group position="apart">
|
||||
<Text>Auto Fit Columns</Text>
|
||||
<Text>
|
||||
{t('table.config.general.autoFitColumns', {
|
||||
postProcess: 'titleCase',
|
||||
})}
|
||||
</Text>
|
||||
<Switch
|
||||
defaultChecked={table.autoFit}
|
||||
onChange={handleAutoFitColumns}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { useContainerQuery } from '/@/renderer/hooks';
|
|||
import { PlaylistListFilter, useCurrentServer, useListStoreActions } from '/@/renderer/store';
|
||||
import { ListDisplayType, ServerType } from '/@/renderer/types';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RiFileAddFill } from 'react-icons/ri';
|
||||
import { LibraryItem } from '/@/renderer/api/types';
|
||||
import { useListFilterRefresh } from '../../../hooks/use-list-filter-refresh';
|
||||
|
|
@ -24,6 +25,7 @@ interface PlaylistListHeaderProps {
|
|||
}
|
||||
|
||||
export const PlaylistListHeader = ({ itemCount, tableRef, gridRef }: PlaylistListHeaderProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { pageKey } = useListContext();
|
||||
const cq = useContainerQuery();
|
||||
const server = useCurrentServer();
|
||||
|
|
@ -37,7 +39,7 @@ export const PlaylistListHeader = ({ itemCount, tableRef, gridRef }: PlaylistLis
|
|||
tableRef?.current?.api?.purgeInfiniteCache();
|
||||
},
|
||||
size: server?.type === ServerType?.NAVIDROME ? 'xl' : 'sm',
|
||||
title: 'Create Playlist',
|
||||
title: t('form.createPlaylist.title', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -74,7 +76,9 @@ export const PlaylistListHeader = ({ itemCount, tableRef, gridRef }: PlaylistLis
|
|||
w="100%"
|
||||
>
|
||||
<LibraryHeaderBar>
|
||||
<LibraryHeaderBar.Title>Playlists</LibraryHeaderBar.Title>
|
||||
<LibraryHeaderBar.Title>
|
||||
{t('page.playlistList.title', { postProcess: 'titleCase' })}
|
||||
</LibraryHeaderBar.Title>
|
||||
<Paper
|
||||
fw="600"
|
||||
px="1rem"
|
||||
|
|
@ -88,7 +92,10 @@ export const PlaylistListHeader = ({ itemCount, tableRef, gridRef }: PlaylistLis
|
|||
)}
|
||||
</Paper>
|
||||
<Button
|
||||
tooltip={{ label: 'Create playlist', openDelay: 500 }}
|
||||
tooltip={{
|
||||
label: t('action.createPlaylist', { postProcess: 'sentenceCase' }),
|
||||
openDelay: 500,
|
||||
}}
|
||||
variant="filled"
|
||||
onClick={handleCreatePlaylistModal}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
convertQueryGroupToNDQuery,
|
||||
} from '/@/renderer/features/playlists/utils';
|
||||
import { QueryBuilderGroup, QueryBuilderRule } from '/@/renderer/types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RiMore2Fill, RiSaveLine } from 'react-icons/ri';
|
||||
import { SongListSort } from '/@/renderer/api/types';
|
||||
import {
|
||||
|
|
@ -86,6 +87,7 @@ export const PlaylistQueryBuilder = forwardRef(
|
|||
{ sortOrder, sortBy, limit, isSaving, query, onSave, onSaveAs }: PlaylistQueryBuilderProps,
|
||||
ref: Ref<PlaylistQueryBuilderRef>,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const [filters, setFilters] = useState<QueryBuilderGroup>(
|
||||
query ? convertNDQueryToQueryGroup(query) : DEFAULT_QUERY,
|
||||
);
|
||||
|
|
@ -354,7 +356,11 @@ export const PlaylistQueryBuilder = forwardRef(
|
|||
};
|
||||
|
||||
const sortOptions = [
|
||||
{ label: 'Random', type: 'string', value: 'random' },
|
||||
{
|
||||
label: t('filter.random', { postProcess: 'titleCase' }),
|
||||
type: 'string',
|
||||
value: 'random',
|
||||
},
|
||||
...NDSongQueryFields,
|
||||
];
|
||||
|
||||
|
|
@ -414,21 +420,21 @@ export const PlaylistQueryBuilder = forwardRef(
|
|||
<Select
|
||||
data={[
|
||||
{
|
||||
label: 'Ascending',
|
||||
label: t('common.ascending', { postProcess: 'titleCase' }),
|
||||
value: 'asc',
|
||||
},
|
||||
{
|
||||
label: 'Descending',
|
||||
label: t('common.descending', { postProcess: 'titleCase' }),
|
||||
value: 'desc',
|
||||
},
|
||||
]}
|
||||
label="Order"
|
||||
label={t('common.order', { postProcess: 'titleCase' })}
|
||||
maxWidth="20%"
|
||||
width={125}
|
||||
{...extraFiltersForm.getInputProps('sortOrder')}
|
||||
/>
|
||||
<NumberInput
|
||||
label="Limit"
|
||||
label={t('common.limit', { postProcess: 'titleCase' })}
|
||||
maxWidth="20%"
|
||||
width={75}
|
||||
{...extraFiltersForm.getInputProps('limit')}
|
||||
|
|
@ -444,7 +450,7 @@ export const PlaylistQueryBuilder = forwardRef(
|
|||
variant="filled"
|
||||
onClick={handleSaveAs}
|
||||
>
|
||||
Save as
|
||||
{t('common.saveAs', { postProcess: 'titleCase' })}
|
||||
</Button>
|
||||
<DropdownMenu position="bottom-end">
|
||||
<DropdownMenu.Target>
|
||||
|
|
@ -462,7 +468,7 @@ export const PlaylistQueryBuilder = forwardRef(
|
|||
icon={<RiSaveLine color="var(--danger-color)" />}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save and replace
|
||||
{t('common.saveAndReplace', { postProcess: 'titleCase' })}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { CreatePlaylistBody, CreatePlaylistResponse, ServerType } from '/@/rende
|
|||
import { Button, Switch, TextInput, toast } from '/@/renderer/components';
|
||||
import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/create-playlist-mutation';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface SaveAsPlaylistFormProps {
|
||||
body: Partial<CreatePlaylistBody>;
|
||||
|
|
@ -18,6 +19,7 @@ export const SaveAsPlaylistForm = ({
|
|||
onSuccess,
|
||||
onCancel,
|
||||
}: SaveAsPlaylistFormProps) => {
|
||||
const { t } = useTranslation();
|
||||
const mutation = useCreatePlaylist({});
|
||||
const server = useCurrentServer();
|
||||
|
||||
|
|
@ -40,10 +42,15 @@ export const SaveAsPlaylistForm = ({
|
|||
{ body: values, serverId },
|
||||
{
|
||||
onError: (err) => {
|
||||
toast.error({ message: err.message, title: 'Error creating playlist' });
|
||||
toast.error({
|
||||
message: err.message,
|
||||
title: t('error.genericError', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
toast.success({ message: `Playlist has been created` });
|
||||
toast.success({
|
||||
message: t('form.createPlaylist.success', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
onSuccess(data);
|
||||
onCancel();
|
||||
},
|
||||
|
|
@ -60,16 +67,25 @@ export const SaveAsPlaylistForm = ({
|
|||
<TextInput
|
||||
data-autofocus
|
||||
required
|
||||
label="Name"
|
||||
label={t('form.createPlaylist.input', {
|
||||
context: 'name',
|
||||
postProcess: 'titleCase',
|
||||
})}
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
<TextInput
|
||||
label="Description"
|
||||
label={t('form.createPlaylist.input', {
|
||||
context: 'description',
|
||||
postProcess: 'titleCase',
|
||||
})}
|
||||
{...form.getInputProps('comment')}
|
||||
/>
|
||||
{isPublicDisplayed && (
|
||||
<Switch
|
||||
label="Is Public?"
|
||||
label={t('form.createPlaylist.input', {
|
||||
context: 'public',
|
||||
postProcess: 'titleCase',
|
||||
})}
|
||||
{...form.getInputProps('_custom.navidrome.public', { type: 'checkbox' })}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -78,7 +94,7 @@ export const SaveAsPlaylistForm = ({
|
|||
variant="subtle"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
{t('common.cancel', { postProcess: 'titleCase' })}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isSubmitDisabled}
|
||||
|
|
@ -86,7 +102,7 @@ export const SaveAsPlaylistForm = ({
|
|||
type="submit"
|
||||
variant="filled"
|
||||
>
|
||||
Save
|
||||
{t('common.save', { postProcess: 'titleCase' })}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import { Button, Select, Switch, TextInput, toast } from '/@/renderer/components
|
|||
import { useUpdatePlaylist } from '/@/renderer/features/playlists/mutations/update-playlist-mutation';
|
||||
import { queryClient } from '/@/renderer/lib/react-query';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '/@/i18n/i18n';
|
||||
|
||||
interface UpdatePlaylistFormProps {
|
||||
body: Partial<UpdatePlaylistBody>;
|
||||
|
|
@ -27,6 +29,7 @@ interface UpdatePlaylistFormProps {
|
|||
}
|
||||
|
||||
export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlaylistFormProps) => {
|
||||
const { t } = useTranslation();
|
||||
const mutation = useUpdatePlaylist({});
|
||||
const server = useCurrentServer();
|
||||
|
||||
|
|
@ -60,10 +63,12 @@ export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlayl
|
|||
},
|
||||
{
|
||||
onError: (err) => {
|
||||
toast.error({ message: err.message, title: 'Error updating playlist' });
|
||||
toast.error({
|
||||
message: err.message,
|
||||
title: t('error.genericError', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success({ message: `Playlist has been saved` });
|
||||
onCancel();
|
||||
},
|
||||
},
|
||||
|
|
@ -80,23 +85,35 @@ export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlayl
|
|||
<TextInput
|
||||
data-autofocus
|
||||
required
|
||||
label="Name"
|
||||
label={t('form.createPlaylist.input', {
|
||||
context: 'name',
|
||||
postProcess: 'titleCase',
|
||||
})}
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
<TextInput
|
||||
label="Description"
|
||||
label={t('form.createPlaylist.input', {
|
||||
context: 'description',
|
||||
postProcess: 'titleCase',
|
||||
})}
|
||||
{...form.getInputProps('comment')}
|
||||
/>
|
||||
{isOwnerDisplayed && (
|
||||
<Select
|
||||
data={userList || []}
|
||||
{...form.getInputProps('_custom.navidrome.ownerId')}
|
||||
label="Owner"
|
||||
label={t('form.createPlaylist.input', {
|
||||
context: 'owner',
|
||||
postProcess: 'titleCase',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{isPublicDisplayed && (
|
||||
<Switch
|
||||
label="Is Public?"
|
||||
label={t('form.createPlaylist.input', {
|
||||
context: 'public',
|
||||
postProcess: 'titleCase',
|
||||
})}
|
||||
{...form.getInputProps('_custom.navidrome.public', { type: 'checkbox' })}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -105,7 +122,7 @@ export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlayl
|
|||
variant="subtle"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
{t('common.cancel', { postProcess: 'titleCase' })}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isSubmitDisabled}
|
||||
|
|
@ -113,7 +130,7 @@ export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlayl
|
|||
type="submit"
|
||||
variant="filled"
|
||||
>
|
||||
Save
|
||||
{t('common.save', { postProcess: 'titleCase' })}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
|
@ -166,6 +183,6 @@ export const openUpdatePlaylistModal = async (args: {
|
|||
onCancel={closeAllModals}
|
||||
/>
|
||||
),
|
||||
title: 'Edit playlist',
|
||||
title: i18n.t('form.editPlaylist.title', { postProcess: 'titleCase' }) as string,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useRef, useState } from 'react';
|
|||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import { Box, Group } from '@mantine/core';
|
||||
import { closeAllModals, openModal } from '@mantine/modals';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RiArrowDownSLine, RiArrowUpSLine } from 'react-icons/ri';
|
||||
import { generatePath, useNavigate, useParams } from 'react-router';
|
||||
import { PlaylistDetailSongListContent } from '../components/playlist-detail-song-list-content';
|
||||
|
|
@ -19,6 +20,7 @@ import { PlaylistSongListQuery, ServerType, SongListSort, SortOrder } from '/@/r
|
|||
import { usePlaylistSongList } from '/@/renderer/features/playlists/queries/playlist-song-list-query';
|
||||
|
||||
const PlaylistDetailSongListRoute = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const tableRef = useRef<AgGridReactType | null>(null);
|
||||
const { playlistId } = useParams() as { playlistId: string };
|
||||
|
|
@ -114,7 +116,7 @@ const PlaylistDetailSongListRoute = () => {
|
|||
}
|
||||
/>
|
||||
),
|
||||
title: 'Save as',
|
||||
title: t('common.saveAs', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue